DEV Community

mattewens
mattewens

Posted on • Originally published at callforge.dev

What Breaks When Your Voice Agent Hits Customer #10

Three real failures from shipping voice SaaS, and the boring fixes nobody demos


Customer #1 is easy. One phone number, one Vapi assistant, one Stripe subscription you clicked together by hand in the dashboard. Your demo works. Landing page is live. You're ready to scale.

Customer #10 is where it gets interesting.

Same code. Same config. Suddenly everything's on fire. I've hit this wall three times now, building voice-agent SaaS for consulting clients, and honestly the pattern's identical every single time. Here's what actually breaks when you cross that threshold, plus the unsexy infra work that saves you.

The Provisioning Problem

Your first customer gets a Twilio number you bought manually. You hardcoded it into the env vars. It's fine. They're happy. Life is good.

Your tenth customer needs a number too. So you buy one, drop it in the config, redeploy. Still fine. Annoying, but fine.

Your twentieth customer hits at 11pm on a Sunday. You're at dinner. They're in a different timezone and their number isn't working because something broke in your manual process, and now you're debugging Twilio logs on your phone while the restaurant staff give you that look. You know the one.

The real fix isn't sexy. It's a provisioning API that buys numbers automatically. Health checks that retry failed purchases. A pool of warm numbers sitting there ready to assign. All the stuff you skip in the tutorial because "we'll add that later."

Later never comes. Until it burns you.

The boring fix:

// Auto-provision numbers with health checks
async function provisionNumber(customerId: string) {
  const pool = await getWarmNumberPool();
  const number = pool.pop() || await twilio.incomingPhoneNumbers.create({
    areaCode: '415',
    voiceUrl: `${BASE_URL}/webhook/voice/${customerId}`
  });

  // Health check before assignment
  await validateNumber(number.phoneNumber);
  await assignToCustomer(customerId, number.sid);

  // Replenish pool
  if (pool.length < 5) await warmNumberPool(3);
}
Enter fullscreen mode Exit fullscreen mode

The Billing Revelation

Look, seat-based pricing is a trap for voice. Your customer doesn't care about seats. They care about minutes. One agent handling 1000 calls is a completely different beast from ten agents handling ten calls each. Seat pricing makes zero sense here.

I learned this the expensive way. A client had a customer go viral on TikTok. Their phone line just blew up. Weekend call volume hit 50,000 minutes. Under seat pricing? That customer paid $49. Their Twilio bill was $1,247.

That's a fun Monday morning conversation.

Real voice SaaS needs metered billing. Twilio call-status webhooks feeding into Stripe usage records, per-minute billing that tracks what people actually use, not some artificial seat count. It's tedious to build. Everyone skips it. Everyone regrets it.

The boring fix:

// Webhook handler for usage-based billing
app.post('/webhook/twilio/call-status', async (req, res) => {
  const { CallSid, CallDuration, CallStatus } = req.body;

  if (CallStatus === 'completed') {
    const customer = await getCustomerByCallSid(CallSid);
    const minutes = Math.ceil(parseInt(CallDuration) / 60);

    // Push to Stripe usage records
    await stripe.subscriptionItems.createUsageRecord(
      customer.stripeSubscriptionItemId,
      {
        quantity: minutes,
        timestamp: Math.floor(Date.now() / 1000),
        action: 'increment'
      }
    );
  }

  res.sendStatus(200);
});
Enter fullscreen mode Exit fullscreen mode

The Compliance Trap

GDPR sounds theoretical until your first erasure request lands in your inbox. "Please delete all my data."

Simple, right? Except your recordings are scattered across three S3 buckets. Your call logs live in Twilio. Your transcripts are sitting in your LLM provider. Your customer database is in Postgres. And you never built a way to connect any of it.

So now you're grepping through logs at midnight trying to find every trace of one customer, while their lawyer sends increasingly pointed emails. Cool cool cool.

The fix is per-tenant isolation from day one. Separate buckets, consent gates, erasure hooks that cascade through your data model. It's boring infrastructure work. Absolutely critical.

The boring fix:

// Per-tenant storage with cascade erasure
async function eraseCustomerData(customerId: string) {
  const tenant = await getTenant(customerId);

  // S3: Delete all objects in tenant bucket
  await s3.deleteObjects({
    Bucket: `callforge-recordings-${tenant.bucketSuffix}`,
    Delete: await listAllObjects(tenant.bucketSuffix)
  });

  // Twilio: Delete recordings via API
  const recordings = await twilio.recordings.list({
    dateCreatedAfter: tenant.createdAt,
    dateCreatedBefore: new Date()
  });
  await Promise.all(recordings.map(r => r.remove()));

  // LLM provider: Delete stored transcripts
  await deleteLLMTranscripts(tenant.llmSessionIds);

  // Finally: Remove from database
  await db.customers.delete({ where: { id: customerId } });
}
Enter fullscreen mode Exit fullscreen mode

What This Means for Your Stack

Here's the thing. These aren't edge cases. They're the difference between a demo and a business. Every voice-agent tutorial out there shows you how to wire Vapi to Twilio in twenty lines of code. Nobody shows you the three months of infrastructure work that follows.

We learned this by building voice SaaS for clients, and by tearing down 25 open-source voice-agent repos to see where everyone skips the exact same steps. Spoiler: it's always the same steps.

If you're serious about shipping voice agents that don't fall over at customer #10, start with the boring stuff:

  • Automated provisioning with health checks
  • Metered billing tied to actual usage
  • Per-tenant data isolation with erasure hooks

The flashy features can wait. Your 3am self will thank you.


Check the teardown: I analyzed 25 voice-agent repos to find exactly where teams skip this infrastructure. See the full breakdown at callforge.dev/teardown, or grab the boilerplate that already handles provisioning, billing, and compliance out of the box.

Top comments (0)