Skip to content

Allow this site to count your visits with Yandex Metrica? Metrica will store cookies in your browser to do this. How the site uses cookies

VC
Case 2 of 25 · E-commerce · Support

After a missed call, the customer gets an SMS with the real ship date

The cloud phone system reports a missed call. My service then finds the customer and their orders in two Shopify stores and sends an SMS with the actual shipping status. The CRM also got a "Missed calls" page: who called, what's in their orders, and a reply ready to send.

Industry
E-commerce, US customers
Technology
Python · phone system webhooks · Shopify API
Timeline
About 2 weeks
Outcome
~180 missed calls a month get an automatic reply
01 · Problem

Customers call during their day, when the support team's day is already over

The client is a US outdoor-gear brand with two Shopify stores and a support team in another time zone. A customer calls in the middle of their working day, when it is evening or night for the team. Over 90 days the call log holds 739 inbound calls. Of these, 496 came in during the advertised business hours, and 310 of those went unanswered.

Missed calls weren't collected anywhere. The morning began with a long feed of notifications in the phone app: usually about 5 a night, up to 25 on peak days. Going through it by hand is a job in itself, and nobody did it consistently.

The most common question was "where is my order?" People called precisely because they couldn't see a status: the products are expensive and take a long time to make, and nothing new showed up in emails or in the customer account.

The phone system's built-in auto-reply didn't help: at night people got the daytime "we'll call you back shortly". On one of the lines the business-hours switch was off altogether, so that template went to everyone, all the time.

02 · Solution

The phone system reports the call, and my service finds the order and sends an SMS with the real date

After every call, the phone system sends the CRM a webhook, an automatic call.completed notification. The receiver checks the signature, drops event types it doesn't need, makes sure the same call isn't processed twice (otherwise the customer would get two messages) and writes everything to a log. Then the system uses the caller's number to find the customer and their orders in both stores, and puts the reply together.

01
Call notification

call.completed from the cloud phone system: signature check, event filtering, protection against processing a call twice

02
Customer lookup

The caller's number is used to find the customer and their orders in two Shopify stores

03
Reply steps

From the real ship date down to a neutral fallback reply

04
Sending the SMS

Quiet hours, a pause between messages to one number, a rate limit, draft mode

05
CRM queue

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

The key finding: the order has no ship date

The central question of the whole system is where to get a date you aren't embarrassed to send a customer. It turned out that a Shopify order has no calendar ship date anywhere. I checked four other sources, and none of them held the date the customer had been promised.

The promised date lives in only one place: the name of the shipping option the buyer chose at checkout. That is where they saw the timeframe and agreed to it. The date is worked out from that text. The whole two weeks of work hinged on this one finding, and it had to come before any code was written.

The reply is built in steps

The reply is chosen step by step, from the most specific to neutral. No step is allowed to mislead the customer:

  • customer and order found, promised date still ahead → give the date
  • promised date already passed → don't repeat it; say honestly that we're checking the date with the warehouse
  • the order exists but no date can be worked out → mention the order without inventing a promise
  • no customer matched the number → a neutral reply, and the call goes into the queue for a person

The rule "never repeat an overdue date" was checked against an export of the full order history (more than 11,000 orders): not a single old, stuck order gets a date in the future.

Safeguards on sending

An automatic SMS goes out in the company's name, and nobody sees it before it's sent. So the limits matter more than the text itself:

  • quiet hours: messages go out only between 11:00 and 21:00 New York time, which is a reasonable hour in every time zone of the continental US; Alaska and Hawaii are handled separately
  • a pause for each number: if someone calls again, they don't get a stream of messages
  • an overall rate limit on sending
  • draft mode: everything is calculated and logged, but nothing goes out; live sending is turned on with a separate switch

The "Missed calls" page in the CRM

The system sends the first reply itself, but a person still has to deal with the calls. There is a separate page for that: who called and when, the number as a tel: link you can call right away, the matched customer, their orders with status ("unfulfilled · 82 days"), a ready reply and a "handled" checkbox.

The page survives a partial failure: one broken line in the queue doesn't bring it down. If the queue can't be read, the page says so: "queue could not be read". Had it said "no calls", the operator would calmly have closed the tab.

03 · Technology

Only what's needed: the system has to run through the night with nobody on duty

Python (standard library)

Receives notifications and checks the signature (HMAC) without a web framework

Cloud phone system

Call notifications, SMS sending, voicemail transcripts

Shopify Admin API

Finds the customer by phone number and their orders in two stores

Language model

Writes the reply text for the specific order

systemd

The receiver service, automatic restart, log

pytest

The shared set of 2,471 CRM and analytics tests, including ones for the reply steps, quiet hours, duplicate protection and event parsing

PythonWebhooksHMACSMS APIShopify Admin APILLMsystemdpytest
04 · Results

Data first, code second

Actual scale
108 ~180

missed calls a month: the problem turned out about two-thirds bigger than first estimated

Transcripts already there
29%

of missed calls have a voicemail that is already transcribed, which shows what the call was about

Cost of sending
62%

of messages now fit into one SMS segment after one character was replaced

The work started with an export from the phone system. It showed three facts that appeared in no report. The problem was about two-thirds bigger than first estimated. Already 29% of missed calls had a voicemail transcript, and those transcripts now show what each call was about. And 36% of customer text messages never got a reply from a person.

I fixed five bugs along the way, and three of them had stopped the system from working at all

  • the customer lookup crashed on mismatched function parameters, so every call was treated as "customer not found"
  • event parsing looked at the wrong level of the data and silently threw away every missed call
  • the caller's number was read from a field the notification doesn't have
  • the auto-reply card brought down the whole calls page, the page two staff members start their morning with. The code was written against guessed field names, and the test data was made up the same way, so the test only confirmed the author's guess
  • the missed-call totals were written and covered by tests, but nothing called them. The manager kept asking how much the missed calls were costing and got no answer, although it was already in the code

The first three were invisible from the outside: the service ran, logs were written, and no SMS went out. Everyone noticed the fourth at once, because the calls page stopped opening. The fifth showed up only as silence: the code for the totals existed, but nobody ever saw the numbers.

How to check that notifications are getting through

Silence in the system can mean two things: there really were no calls, or the notifications stopped arriving. As long as the two look the same, a report about a quiet night is worth nothing. So the delivery of notifications is checked separately:

  • The receiver ran code almost three weeks old for 18 days. The restart counter showed zero, and the file checksum on disk matched the repository, but that proved nothing: you have to compare the process start time with the file modification time. The consequence was exactly what this case is about. The customer lookup by phone number went out as a request to another server, an empty response made the function silently return nothing, and the call turned into "customer not found". Before the restart, the code was checked against the repository byte for byte, and 732 tests of this part of the system were run; all of them passed
  • The "were there any calls" check relied on a service method of the phone system's API that returns HTTP 400 with no data. A simple parser prints "0 calls", and the error can't be told apart from proof that no calls came in. This came to light during a control period when calls had definitely come in, yet the method still showed "0". A different method is now used for the check, and notification delivery is verified in four independent ways, none of which depends on the queue
  • The trigger rule was written down in advance: "if no event at all has arrived by 18:00 Monday, check the notification settings". That check was never needed. The weekend explanation held exactly: Friday 4 events, Saturday 0, Sunday 3, Monday 6, and the 41 hours of silence fell on the weekend
  • The watchdog script waiting for an email from the phone provider read the very message it was waiting for, saved it and alerted nobody. The unsubscribe filter searched for its phrase across the whole email, and the provider's ticket system appends the entire earlier thread below. Now only the new part of the email is parsed. The same check turned up a second problem: the script's log held 50 lines, and every one of them was an error. A successful run wrote nothing, so "ran without incident" and "not running at all" looked the same

The same rule applies as on the queue page: "couldn't read" is never shown as "empty".

One character turned out to affect the SMS bill directly. The em dash in the templates fell outside the basic SMS character set, which switched messages to a two-byte encoding where each segment holds less text. Because of this, every message went out as at least two segments. After the em dash was replaced with a hyphen, 62% of messages fit into one segment.

The business owner turned on live sending, and the first messages were delivered. Every missed call now lands in the CRM queue, and the customer gets a reply from the steps above, with the real shipping status whenever the order is found.

When customers actually call

The system had run long enough for the notification log to answer a practical question: which hours should the line be staffed? The measurement uses only the log on disk and calls no outside services. Repeated events are removed by call ID, the last event with a final status counts, and the customer's time zone comes from a phone-number parsing library. That gives 739 inbound calls over 90 days, with the time zone known for every one. Counting everything in one time zone wasn't an option: eight in the morning on the East Coast is five in the morning on the West Coast, and the conclusion would have come out backwards.

  • most calls come at 11:00 in the customer's local time; the 09:00–12:00 window carries 38% of calls
  • 82% of calls fall in business hours, 5% in the evening and 1% at night in the customer's local time, so the idea that people call in the evening didn't hold up
  • Monday gets almost twice as many calls as Friday
  • 496 of the 739 calls came in during the advertised business hours, and 310 of those were missed
  • 530 calls were missed over the 90 days in total, about 180 a month, the same figure the first phone export showed
  • in the daily report, 7 of 9 missed calls fell within the advertised business hours

That gives the number all this was for: if the line closed two hours later, at 17:00, it would have caught 133 more calls over 90 days, about 44 a month. Holidays are calculated by formula, because a table of dates is the kind of thing people forget to extend.

I made a mistake here myself, and fixing it took an extra round. The first version worked out time zones from my own table of area codes and immediately put one of the cities in the wrong zone. A time-zone error quietly shifts the whole peak and leaves no trace on the chart. So I threw the table out and handed number parsing to a library, a Python port of libphonenumber. The report notes one caveat that I couldn't check: an area code may not match where a person actually lives.

The "Yesterday" card on the calls page

Next to the queue there is now a summary of the last 24 hours: how many auto-replies went out, how many didn't, and why. The card collects data from every source, and its test checks exactly that. With all the sources connected, the test expects "not sent: 2" with two reasons. If one source is replaced with a stub, the card shows "not sent: 0", and the test catches the gap at once.

05 · Where it fits

Where else this approach works

Behind this case is a common task: a request comes in, find the customer in the business system, reply with facts about their order. The same setup works wherever requests arrive outside business hours:

  • → Online stores with long delivery times: the question "where is my order?" is answered with the status from the business system
  • → Service and repair shops: a missed call gets an SMS with the work-order stage and the real ready date
  • → Clinics and salons: a call outside opening hours gets an appointment confirmation or the next free slot from the schedule
  • → Any business with customers in another time zone: at night, calls get an answer from a system that knows the facts about the order
  • → Any channel for requests: email, messenger, a web form. Only the event source changes; the reply steps and the sending safeguards stay the same
What I reuse on later projects
  • A notification receiver with signature checks, event filtering and protection against processing a call twice
  • Reply steps with an explicit ban on quoting an overdue date, tested against the order history export before launch
  • Sending safeguards: quiet hours in the customer's time zone, a pause between messages to one number, draft mode and a separate switch for live sending
  • A queue page with a ready reply that survives a partial failure: "couldn't read" is never shown as "empty"
Have a similar task?

If missed calls are piling up, count them first and automate the reply second

I start with an export from your phone system: how many calls are really missed, what people call about, and what data has already piled up without being used. Then I build an auto-reply that uses facts from your business system. In this case, about two weeks passed from the data analysis to the first delivered SMS.