Skip to content
VC
Case Study #23 · E-commerce / Support

Missed calls: auto-SMS with the real ship date instead of a canned reply

A cloud-telephony webhook → customer and order lookup across two Shopify stores → an SMS carrying the actual fulfillment status. Plus a "Missed calls" page in the CRM: who called, what's in their orders, and a ready-to-send reply.

Industry
E-commerce, US customers
Stack
Python · telephony webhooks · Shopify API
Timeline
~2 weeks
Outcome
~200 missed/mo → auto-reply
01 · Pain Point

The customer calls during their business day — it's the middle of the night for support

A US outdoor-gear brand, two Shopify stores, and a support team several time zones away. The customer dials in the middle of their working day — which is evening or night for the team. Roughly 70% of inbound calls went unanswered.

Missed calls were not collected anywhere. The morning started not with a list but with a wall of notifications in the telephony app: a median of 5 overnight, up to 25 on peak days. Working through that feed by hand is a job of its own, and nobody was doing it systematically.

The single most common reason for calling was "where is my order". In other words, people called precisely because they couldn't see a status: an expensive product, a long production lead time, and silence in both the emails and the account page.

The built-in telephony auto-reply made it worse: night callers received a daytime "we'll call you back shortly", and on one of the lines the business-hours toggle was switched off entirely — so the canned message went out to everyone, always.

02 · Solution

Webhook → Shopify order → an SMS with the real date

A call.completed webhook receiver on the CRM side: signature verification, filtering out irrelevant event types, protection against processing the same call twice (otherwise the customer gets two callbacks), and a journal entry. From there the caller's number resolves to a customer and their orders across both stores — and the reply gets assembled.

01
Call webhook

call.completed from cloud telephony: HMAC signature, event-type filter, dedup by call id

02
Customer lookup

Caller number → customer and their orders across two Shopify stores

03
Reply ladder

From "the real ship date" down to a neutral fallback

04
SMS delivery

Quiet hours, per-number cooldown, rate limiting, draft mode

05
CRM queue

"Missed calls" page: who called, their orders, ready-made reply, "handled" checkbox

The key finding: the order has no ship date

The central question of the whole pipeline is where to get a date you're not ashamed to send a customer. It turned out that a calendar ship date does not exist anywhere on the Shopify order. Four other candidate sources were checked and rejected — none of them carries the date the customer was actually promised.

The only carrier of that promise is the name of the shipping tier chosen at checkout: that's where the buyer saw a timeframe and agreed to it. The date is derived from that string. This is the kind of project where two weeks of work hinge on one design decision — and you have to find it before writing code.

The reply ladder: from useful to fallback

The reply is assembled in steps, from maximally specific down to neutral, and no step is allowed to lie:

  • customer and order found, promised date still in the future → state the date
  • promised date already passed → do NOT repeat it; say plainly that we're checking with the warehouse
  • order exists but no date can be derived → reference the order without inventing a promise
  • no customer matched the number → neutral reply, the call goes into the human queue

The "never repeat an overdue date" rule was validated against the historical export (over 11k orders): not a single old, stuck order gets handed a future date.

Safety rails on sending

An auto-SMS goes out under the business's name with no human in the loop. That makes the limiters more important than the generation itself:

  • quiet hours: sending only inside an 11:00–21:00 New York window — safe across all continental US time zones; Alaska and Hawaii are handled separately
  • per-number cooldown — a repeat call doesn't turn into a stream of messages
  • rate limiting for the pipeline as a whole
  • draft mode: everything is computed and logged but nothing leaves the system — plus a separate explicit flag for live sending

The "Missed calls" page in the CRM

The auto-reply covers first contact, but a human still has to work the calls. Instead of a notification feed there is one page: who called and when, the number as a tel: link, the matched customer, their orders with state ("unfulfilled · 82 days"), a ready-to-send reply, and a "handled" checkbox.

Degradation instead of failure is built in: a malformed queue line does not take the page down, and a read failure renders "queue could not be read" rather than "no calls". The difference matters: in the second case the operator calmly closes the tab and walks away.

03 · Stack

Nothing extra — this thing has to survive the night unattended

Python stdlib

Webhook receiver and HMAC signature check without a web framework

Cloud telephony

Call webhooks, SMS API, voicemail transcripts

Shopify Admin API

Customer lookup by phone number and their orders across two stores

LLM generation

Reply text written for the specific order, not slotted into a template

systemd

Receiver service, auto-restart, journal

pytest

Over 100 tests on the pipeline: reply ladder, quiet hours, dedup, event parsing

PythonWebhooksHMACSMS APIShopify Admin APILLMsystemdpytest
04 · Results

Data first, code second

Actual scale
108 ~200

missed calls per month — the problem was 1.6× larger than the initial estimate

Unused channel
29%

of missed calls already carry a voicemail transcript — a ready answer to "what did this person want"

Cost of sending
62%

of messages became single-segment after a one-character fix

The work started with a telephony export, not with code. The analysis showed the problem was 1.6× larger than the initial estimate, that 29% of missed calls already had a voicemail transcript — a channel nobody was using at all — and that 36% of customer SMS never received a human reply. Three facts that appeared in no report.

Three breakages fixed along the way — without which the pipeline was dead

  • customer lookup crashed on a signature mismatch — every call was treated as "customer not found"
  • event parsing read the wrong nesting level and silently discarded every missed call
  • the caller's number was being read from a field the webhook does not contain

None of the three were visible from the outside: the service ran, logs were written, no SMS went out.

A separate find was a one-character detail with a direct effect on the bill: an em dash in the templates pushed messages outside the basic SMS alphabet and into a two-byte encoding, so 100% of messages took at least two segments. Replacing the em dash with a hyphen makes 62% of them single-segment.

The business owner switched live sending on and the first messages were delivered. Every missed call now lands in the CRM queue and gets a reply with a real status — instead of a daytime "we'll call you back shortly" at three in the morning.

05 · Where it fits

Where else the same methodology applies

This isn't "an answering machine". It's the standard task of "an event in a communication channel → pull the customer's context from the system of record → answer with a fact, not a template". The same architecture applies anywhere inquiries arrive outside business hours:

  • Online stores with long lead times — "where is my order" gets closed by a status from the system of record, not by a callback
  • Service and repair shops — missed call → SMS with the work-order stage and the real readiness date
  • Clinics and salons — a call outside opening hours → appointment confirmation or the next open slot from the schedule
  • Any business whose customers sit in another time zone — a night shift replaced by a pipeline that knows the facts about the order
  • Any inbound channel — email, messenger, web form: only the event source changes, the reply ladder and the safety rails stay
What's reused on subsequent projects
  • Webhook receiver with signature verification, event-type filtering and duplicate-processing protection
  • A reply ladder with an explicit ban on quoting an overdue date — dry-run against the historical export before go-live
  • The sending safety-rail set: quiet hours in the customer's time zone, per-number cooldown, draft mode, an explicit live-mode flag
  • A queue page with ready-made reply text and degradation instead of failure: "couldn't read" is never rendered as "empty"
Similar challenge?

If missed calls are piling up — measure first, automate second

I start with an export from your telephony: how many calls are actually missed, what people call about, and what's already sitting in channels nobody reads. Then comes the auto-reply pipeline built on facts from your system of record. In this case: about two weeks from data analysis to the first delivered messages.

Ready to start?

The 5,000 ₽ audit — with a concrete report and quote

I'll tell you what to deploy in your business first, what the payback looks like, and whether you need AI for the task at all (sometimes you don't).

Or just send your question — I reply within 2 hours