Back to blog
FILE 0x99·NIGHTDESK NOW TAKES STRIPE — FROM ROI CALCULATOR TO LIVE SUB

NightDesk now takes Stripe — from ROI calculator to live subscription in one click

June 13, 2026 · nightdesk, stripe, billing, saas, msp

NightDesk has had a landing page with an ROI calculator since launch. You put in your DID count, your average after-hours call volume, and it tells you how much you'd save versus a human answering service. The math is usually compelling — $1,500/month in savings on a $499/month plan is the kind of number that gets a "let's try it" from an MSP owner.

The problem: after the calculator, there was nowhere to go. The CTA was "contact us." That's a conversion dead end.

Billing is live now. The ROI page closes the loop.


The three tiers

Solo   — $199/month   (up to 5 DIDs, 500 minutes/month)
Small  — $499/month   (up to 20 DIDs, 2,000 minutes/month)
Mid    — $1,499/month (up to 100 DIDs, 10,000 minutes/month)

POST /billing/checkout with a plan parameter (solo, small, mid) creates a Stripe Checkout session and returns the URL. The frontend redirects. Stripe handles card collection, SCA, receipts.

curl -X POST https://nightdesk.app/billing/checkout \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"plan": "small"}'
# returns: {"checkout_url": "https://checkout.stripe.com/..."}

Webhook handling

POST /stripe/webhook processes the full subscription lifecycle:

All events write to the same billing_events log table. If a webhook fires twice (Stripe retries), idempotency is keyed on stripe_event_id. The webhook endpoint returns 200 immediately and processes async — Stripe's retry logic needs a fast response, not a confirmation that the database write finished.


Plan enforcement

GET /billing/status returns the current state of an account:

{
  "plan": "small",
  "active": true,
  "dids_limit": 20,
  "minutes_limit": 2000,
  "dids_used": 7,
  "minutes_used": 412,
  "period_end": "2026-07-13"
}

The call routing layer checks active and dids_used / minutes_used before placing calls. If an account is over limit or inactive, it falls through to the voicemail handler rather than silently billing for overages. No surprise charges — just a ceiling and a clear status.


Pilot path

POST /billing/pilot (admin-only) activates a 90-day free pilot on any account — no Stripe involved, no credit card required. Plan enforcement is identical to a paid subscription: real DID limits, real minute limits, real call routing. The only difference is the payment record is absent.

This exists because the sales motion for MSPs is "let it run alongside your existing answering service for 90 days and compare." You can't do that if the pilot is degraded in some way. A pilot account is a full account with a timer.


Why this took this long

The ROI calculator was built first intentionally. I wanted to know whether the math resonated before building any payment infrastructure. If nobody engaged with the calculator, building Stripe integration would have been premature.

The calculator got enough traction — and enough "how do I sign up" messages — that billing became the obvious next thing. Building it after product-market fit validation rather than before means the tier structure is based on what people actually asked about, not what I guessed upfront.

The ROI page now has a "Start subscription" button under each tier result. It calls /billing/checkout and redirects. That's the whole flow.