Thanks everyone for you responses. I finally figured it out so I’m going to leave a rather long winded answer in case anyone else is trying to figure this out in the future (or a LLM that someones using picks up on this response).
High level - the problem was a mixture of what Vapi sends over during and at the end of the call and how n8n handles batches of incoming webhooks.
Vapi
Even if you set the server messages as ‘end of call report’ only, Vapi will still sometimes send a ‘status update’ server message even when you explicitly tell it not to. I manually reviewed close to 1,000 calls and this is what i found from the most common ‘endedReason’ values when I only had ‘end of call report’ configured in the server messages.
- “Twilio failed to connect call”:
- ‘End of call report’ = 66% of time
- ‘Status update’ = 33% of time
- “Customer busy”
- ‘End of call report’ = 50% of time
- ‘Status update’ = 50% of time
- “Customer did not answer’“
- ‘End of call report’ = 63.2% of time
- ‘Status update’ = 36.8% of time
- “Silence timed out”
- ‘End of call report = 100% of time
- “Customer Ended Call”
- ‘End of call report’ = 100% of time
- “Voicemail”
- ‘End of call report’ = 100% of time
So, basically Vapi elects to still send a ‘status update’ webhook in call scenarios that don’t involve the customer actually picking up the phone even if you told it not to in the server messages. What I found helped reduce these percentages is to set the server messages in the assistant overrides of the API call you make to vapi instead of directly in the assistant dashboard found in the Vapi UI. It looks something like this:
“serverMessages”: [
“end-of-call-report”
],
“server”: {
“url”: “{{ $execution.resumeUrl }}”,
“timeoutSeconds”: 20
}
However, this still doesn’t totally eliminate the possibility of a ‘status update’ being sent over when you don’t want it. And that’s where the n8n configuration comes into play…
N8N
Important to note - What I built is a monolithic workflow. I know most people separate their workflows into two pieces for these types of builds … The first flow initiates the call, the second flow usually starts with a webhook and receives the end of call data. However, for those of you on a cloud hosted plan the two workflow approach doesn’t bode well for your monthly execution limits. This is because with that approach you’re, at a bare minimum, doubling the amount of monthly executions you’ll have.
The automation I built handles the call initiation and the end of call digestion all in one. To make this possible I used a wait node that resumes on a webhook call somewhere near the middle of the workflow (this node is what accepts the ‘status update’ or ‘end of cal report’ webhook from vapi). The webhook being called isn’t a static webhook like you’re used to seeing inside the webhook trigger node, but instead it’s the execution specific webhook which looks like this: {{ $execution.resumeUrl }}. When you send this over in your Vapi API call that actually makes the call (see the sample JSON body above), it changes the webhook destination to that particular executions wait node (assuming the wait node is configured to resume on webhook call).
After the wait node, you just need a simple switch node that determines what kind of webhook was received. And, depending on what webhook was received, you either loop the output of the switch node back to the wait node so that it resumes waiting for the correct type of webhook response, or you move it into another Vapi api call that retrieves the end of call report for that particular call ID.
Here’s a picture of what that flow looks like:
And here’s a picture of the configuration for ‘Webhook Received?’ switch node:
Since both the ‘end of call report’ and ‘status update’ webhook responses have the call ID this switch configuration makes it so that whatever passes through the ‘Yes’ and ‘Status Update - Ended’ arms will successfully be able to get the end of call reports from the downstream “Get End of Call Report” HTTP nodes.
Lastly (and this part is important)
For those of you that plan on running high volume of calls in parallel you should know that all n8n plans that aren’t Enterprise process webhooks sequentially instead of in parallel. Basically, this means n8n processes one webhook at a time instead of all at once, and this severely slows everything down. This is a hard limitation that you can’t change (unless you’re on the Enterprise plan) and it basically makes it impossible for the other Vapi automations in n8n that are triggered by webhooks (i.e. book a time on calendar, check availability, etc.) to run in a timely manner.
To solve this you need to set up a self hosted instance. I recommend using Railways pre-packaged n8n deployment (the one that has like 95k+ stars). It has configurable workers (which is what you’ll need to process webhooks in parallel instead of one at a time) and it took me all of 15 minutes to setup (pro tip: setup railways MCP in Claude and it can do debugging and configuration for you very easily). This was my first time ever setting up a self hosted instance and it was surprisingly easy. Costs are super cheap too. Btw none of this last part is an ad, I’m just explaining what I did and how it worked out for me.
Hope this helps!