Skip to content
CaptiFi Connect

Behind the scenes9 min read

Shopify SMS notifications through webhooks and an API

Shopify texts one order confirmation, and only when there's no email. How to send your own Shopify SMS notifications from webhooks, with the HMAC check.

In short

  • Shopify's own SMS notifications send a single order confirmation, and only to customers with no email address on their profile; shipping updates are not sent by SMS.
  • Shopify signs each HTTPS webhook with a base64 HMAC SHA-256 of the raw request body, keyed with the app's client secret, in the X-Shopify-Hmac-SHA256 header.
  • Shopify allows one second to connect and five seconds for a whole webhook request, and retries a failed delivery 8 times over the next 4 hours.
  • Shopify treats a customer's phone number as a protected customer field, so a public app must request access to it and pass review before it can read it.
  • The ONS put online sales at 28.8% of all retail sales in Great Britain in September 2026, in its bulletin published on 23 October 2026.
A person taping up a cardboard box for an order at a desk in a small studio
Photo: Bench Accounting, Unsplash

Shopify will text a customer for you, once. Its help centre is plain about the limits: "A single order confirmation SMS notification is sent to a customer when there's no email address associated with the customer profile. Customers won't receive shipping notifications or other notification types by SMS." Anyone who gives an email address at checkout gets email instead, and can't choose texts. The wording can't be edited, the customer can't reply, and the sender name is the store name squeezed to 11 letters with the spaces taken out ("My Shopify Store" turns up as MyShopifySt). For anything beyond that, Shopify points you to "a transactional SMS app from the Shopify App Store".

The other route is to build your own Shopify SMS notifications: subscribe to the store's webhooks, check each one really came from Shopify, and call an SMS API from your server. It's a small piece of code, and the parts that go wrong are the ones Shopify's developer docs spend most time on, which are the signature check and the five-second deadline. This post follows those docs as they read on 4 November 2026, for API version 2026-10.

There are plenty of orders to text about. The ONS put online sales at 28.8% of all retail sales in Great Britain in September 2026, in the retail bulletin it published on 23 October. Split by store type, the share runs from about one sale in ten for food stores to nearly nine in ten for retailers with no shop at all.

Internet sales as a share of each store type's sales, Great Britain, September 2026
0%25%50%75%100%Non-store retailingNon-store retailing, % of sales online: 88.6%88.6%Household goods storesHousehold goods stores, % of sales online: 28.8%All retailing (excl. fuel)All retailing (excl. fuel), % of sales online: 28.3%Textile, clothing, footwearTextile, clothing, footwear, % of sales online: 27%Predominantly non-food storesPredominantly non-food stores, % of sales online: 25.6%Other storesOther stores, % of sales online: 24.8%Non-specialised storesNon-specialised stores, % of sales online: 21.7%Predominantly food storesPredominantly food stores, % of sales online: 10.3%

Value of internet sales as a proportion of all sales in each store type, not seasonally adjusted. Non-store retailing is mostly online retailers but also includes stalls and markets. The bulletin's seasonally adjusted figure for all retailing is 28.8%.

Source: ONS, Retail Sales Index internet sales, table ISCPNSA3, 23 October 2026

See the figures as a table
 % of sales online (%)
Non-store retailing88.6
Household goods stores28.8
All retailing (excl. fuel)28.3
Textile, clothing, footwear27
Predominantly non-food stores25.6
Other stores24.8
Non-specialised stores21.7
Predominantly food stores10.3

How Shopify webhooks work

Shopify describes webhooks as a way to receive "near-real-time data" about events in a shop, and as "a performant alternative to continuously polling". You subscribe to a topic (a resource and an action, like orders/create), give Shopify an address, and it posts a JSON copy of the resource there each time the event happens.

There are three ways to set up a subscription, and which one you use depends on who you are.

App-specific subscriptions are declared in the app's shopify.app.toml file, under [[webhooks.subscriptions]], with topics and a uri. They apply to every shop that installs the app, and Shopify recommends them. Shop-specific subscriptions are made per shop through the GraphQL Admin API's webhookSubscriptionCreate mutation, with the topic in capitals (ORDERS_CREATE). The difference that matters when something breaks: Shopify won't delete a failing app-specific subscription, but it will delete a failing shop-specific one.

The third way needs no app at all. In the Shopify admin, under Settings, Notifications, Webhooks, a merchant can create a webhook for one event, pick JSON, and paste in a URL. The help centre says these are "signed with an ID consisting of a combination of letters and numbers that's unique to your store", and it refuses some addresses outright: localhost, any URL ending in "internal", the store's own domains and Shopify's. For a single shop sending its own texts, this is often all you need.

Which topics to send texts from

Shopify's webhook reference has more than 200 topics, each with a sample payload. A shop sending order texts needs only a few of them.

  • orders/create "occurs whenever an order is created". That's your order confirmation.
  • orders/paid "occurs whenever an order is paid", which is the better trigger if payment can arrive after the order does.
  • fulfillments/create "occurs whenever a fulfillment is created", and its payload carries tracking_company, tracking_number and tracking_url. That's the dispatch text with a tracking link in it.
  • fulfillment_events/create fires for each event added to a fulfilment, with a status such as in_transit and an estimated_delivery_at where there is one.
  • orders/cancelled is the one people forget, and the text that saves a phone call.

There's also customers_marketing_consent/update, which "occurs whenever a customer's SMS marketing consent is updated". Use it to keep your own opt-in records in step with what the customer ticked at checkout, ready for the day you decide to send something promotional.

Each subscription can take include_fields, a list of field paths, so the payload carries only what you need (the order id and name, the phone fields, the first name) and not the whole order. A filter expression in Shopify's search syntax can suppress deliveries you don't want, and Shopify evaluates it against the payload before sending. Both cut down the personal data passing through your server, which is a good habit with order data.

Getting a phone number you can text

An order payload has several places a number can live: phone on the order, shipping_address.phone, billing_address.phone and customer.phone. Any of them can be null. A sensible order of preference is the shipping address phone (it's who is waiting for the parcel), then the order's own phone, then the customer's.

Before your app sees any of those numbers, Shopify has to let it. Its protected customer data rules sort access into levels, and level 2 is "Customer data including name, address, phone, or email fields". Phone is one of four protected customer fields that an app requests "individually because they directly identify customers". A public app has to ask for it in the Partner Dashboard, give its reasons and pass review; for a custom app, level 2 is "always available", and for a custom app created in the admin it "varies by plan". Unapproved fields come back redacted. If your texts silently stop going and every number is empty, check that first.

Then tidy the number. Shopify's help centre says numbers should be in E.164 (+447700900123) for its own SMS to arrive, and customers type all sorts. Our API takes any common UK format and lists anything it can't text under rejected with a reason, such as not_mobile for a landline or unsupported_destination for a number outside the UK. Our post on formatting UK numbers for E.164 covers the cases worth catching at checkout.

Checking Shopify's HMAC before anything else

Your endpoint is a public URL, so anyone who finds it can post a fake order to it. Shopify's answer is a signature on every HTTPS delivery: "a base64-encoded HMAC signature in the X-Shopify-Hmac-SHA256 header, generated using your app's client secret and the raw request body". The docs say to "Always verify HMAC before trusting payload contents".

The manual check, as Shopify describes it, is to compute HMAC SHA-256 over the raw body with the client secret as the key and compare it with the header. Shopify's own Node.js example does three things worth copying.

  1. It reads the body raw (express.raw), before anything parses it. Shopify warns that a JSON body parser placed first "parses the body before your verification code runs", and once the bytes have been re-serialised the HMAC won't match.
  2. It digests to base64, the same encoding as the header. (Our own webhooks send a hex string, so code copied from one won't check the other.)
  3. It compares with crypto.timingSafeEqual on the decoded bytes, and answers 401 when they differ.

Two more lines from the docs save confusion later. After you rotate the client secret, "it can take up to an hour" before Shopify signs with the new one, so accept both for that hour. And deliveries through Google Cloud Pub/Sub or Amazon EventBridge carry no HMAC to check; Shopify says they "don't require it". Our post on webhook signatures explains why the comparison has to be constant time.

Answer in five seconds, text afterwards

Shopify's deadline is short: "a one-second connection timeout and a five-second timeout for the entire request". Anything outside the 200 range counts as a failure, redirects included. Failed deliveries are retried "8 times over the next 4 hours", and after 8 consecutive failures a subscription made through the Admin API is deleted.

So don't call the SMS API inside the webhook handler. Check the HMAC, save the payload, answer 200 and send the text from a queue. An SMS API is usually quick, but a slow moment on the network would push you past five seconds, Shopify would retry, and you'd be texting off the retry as well.

Duplicates come anyway. Shopify says your app "might receive the same webhook more than once", and gives two headers to deal with it. X-Shopify-Webhook-Id is unique per delivery, so store it and skip any you've seen. X-Shopify-Event-Id is shared by every delivery from the same merchant action, which matters if two subscriptions cover one topic. Order isn't promised either: Shopify "doesn't guarantee ordering within a topic", so use X-Shopify-Triggered-At or updated_at to decide what's newest. And because "webhook delivery isn't always guaranteed", the docs recommend a reconciliation job that fetches recently updated orders now and then. For texts, that job is how you catch the dispatch that never produced a webhook.

Calling the SMS API

The send itself is one request. With CaptiFi Connect it's POST /messages with Authorization: Bearer and your key, and a JSON body with to and body. Two optional extras earn their place here.

The first is an Idempotency-Key header built from the order and the stage, such as order-1042-shipped. A retry with the same key and body returns the first answer and sends nothing, so a duplicate webhook, a second subscription or a re-run of the reconciliation job can't text the customer twice. Build it from the order and the stage, because a second subscription to the same topic gets a webhook id of its own. Our post on sending a text exactly once has the retry loop.

The second is client_ref, your own reference of up to 64 characters. Set it to the Shopify order name and it comes back on every delivery report webhook we send, so a failed text can be matched to the order without a phone number in your logs.

The texts themselves are short. For a shop called Fernley Tea:

Fernley Tea: thanks, Sam. Order #1042 is in and we'll pack it tomorrow. We'll text when it's on its way.
Fernley Tea: order #1042 is on its way with Royal Mail. Track it at example.co.uk/t/1042
Fernley Tea: sorry, order #1042 has been cancelled and your refund of £18.50 is on its way. Reply if that's not right.

These are service messages under the ICO's guidance, which covers messages "for administrative or customer services purposes". Add "and 10% off your next order" and the same text becomes direct marketing, with consent rules attached. Our order and delivery updates page has more wording.

Build it with a cc_test_ key first. Test sends go through the same checks, cost nothing, reach no phone and still fire our signed webhooks, so the whole loop from Shopify order to delivery report can be tested on a development store. When you go live, handle a 402 (the balance is empty and nothing was sent) and a 429 (wait for the Retry-After seconds, then retry with the same idempotency key). If you'd like customers to be able to reply, send from a number on a two-way setup. A sender name can't take replies (Shopify's own texts can't either), and a reply of "can you leave it with next door?" is worth getting.

Common questions

Does Shopify send SMS notifications in the UK?

Yes, the United Kingdom is one of twelve countries where Shopify's SMS notifications work. They only cover a single order confirmation, and only for customers who checked out with a phone number and no email address. Shipping and other updates are not sent by SMS.

Which Shopify webhook has the tracking number?

fulfillments/create. Its payload includes tracking_company, tracking_number and tracking_url, plus the order_id it belongs to. fulfillment_events/create follows with later events on the same fulfilment, each with a status.

Not for a factual update about an order they placed. The ICO treats messages for "administrative or customer services purposes" as service messages, outside the marketing consent rules. The moment the text promotes something, it is marketing and needs consent or the soft opt-in.

Can I send Shopify order texts without building an app?

Yes, for a single shop. Create a webhook in the admin under Settings, Notifications, Webhooks and point it at a small script on your own server, which checks the signature and calls the SMS API. You still need somewhere to run that script, because the webhook has to land on an address you control.

Sources

Read for this post on 4 November 2026.

Written by the CaptiFi Connect team. Published 4 November 2026. More in Behind the scenes

Send your first text this week

Set up your account and contacts now. Until we've approved it, a person reads each text before it goes out.