AuthShopifyBackendCustomer Accounts

Shopify CAPI Authentication: Going Along With Shopify, Not Against It

Shopify only does email login, so we built our own phone and OTP login on our own page. That leaves two separate memories of who is signed in, and neither automatically knows what the other is thinking. Here is how we keep them in sync.

Pratham BhatiaPratham BhatiaSDE-1
Aug 21, 2026
8 min read
Shopify CAPI Authentication: Going Along With Shopify, Not Against It
Fig. 01A dispatch on auth
Share this article

Most headless Shopify integrations treat the platform's login screen as something to work around. Hide it. Skip it. Route the customer somewhere else entirely.

We did the opposite. We let Shopify keep asking for a login screen — and just made sure our screen is the one that shows up. Here's why, and what it actually took to make that work.

What is Shopify's Customer Account API?

Shopify's old headless login method, Multipass, is being retired. Its replacement is the Customer Account API — an OAuth flow where the browser always passes through shopify.com at some point. That part isn't optional. It's baked into how OAuth works, and nobody building on this API escapes it.

What is optional is whose login screen the customer actually sees. By default, it's Shopify's own — and Shopify's own screen only takes an email. No phone number. No OTP.

In India, almost everyone signs in by phone. That's not a design preference we could route around. It's a hard mismatch with what Shopify ships out of the box.

The fix: become Shopify's identity provider

Instead of fighting that constraint, we registered our own login page as Shopify's identity provider. Now, whenever Shopify needs someone identified, it comes to us — instead of rendering its own screen.

The result: the only login screen a customer ever sees is ours. Phone number, OTP, done. The browser still transits shopify.com along the way, but no Shopify login screen ever appears on it.

Shopify's own login screen sits behind, dimmed and never shown, while our own phone and OTP login page sits in front, which is all the customer ever sees

Phone and email are linked to the same customer at signup, so a phone login on our side resolves to a real Shopify customer on theirs. Checkout and order history keep working exactly as Shopify expects.

The catch nobody warns you about

Here's what that setup actually costs: two separate memories of who is logged in. Ours, and Shopify's.

Most of the time they agree. But neither system updates or erases the other automatically — and that one fact explains almost everything about how login and logout behave in this architecture. We handled every way the two can drift apart, deliberately, instead of hoping they'd stay in sync.

It's live on bombayshirts.com today. Five scenarios cover it.

The five scenarios at a glance: a normal logout, our session gone while Shopify's survives, a fresh login meeting an old Shopify session, Shopify starting the logout, and Shopify starting the login

Scenario 1: A normal logout

The customer clicks "Log out." It looks like one click. Underneath, two separate memories need updating, and the order matters:

  • Read the id token first, while the record still exists. Shopify will only end its session if we hand back the id token stored on our record. Delete the record first, and that token is gone for good.
  • Build Shopify's logout link from it. buildShopifyLogoutUrl produces the same logout Shopify would run if the customer clicked its own logout button.
  • Delete our own session record. From our side, they're now logged out.
  • Clear our short-lived cache. A separate cache says "trust this session for about a minute" — skip clearing it, and a logged-out session keeps working for that minute.

Then the browser makes a quick trip through Shopify's logout link and comes straight back. It's a redirect, not a screen — the customer sees nothing, but Shopify's session genuinely ends.

Scenario 1: a normal logout clears both our session and Shopify's session in order

Once all four steps finish, both sides agree: nobody is logged in, anywhere.

Scenario 2: We say logged out, Shopify disagrees

This isn't something we build on purpose. It's just what can happen once two separate memories exist.

Why it happens. Our side can lose track of a session without a clean goodbye — it expires naturally, gets removed for some other reason, or a logout attempt fires but the final call to Shopify fails to land.

What it looks like. Our website correctly reports the customer as logged out — from our own records, there's nothing left to say otherwise. But Shopify was never told. It only updates when something explicitly asks it to, so its memory just keeps going.

Scenario 2: our website has logged the customer out, but Shopify still has an active session for them

Nothing breaks immediately here. But it's exactly the setup that makes the next scenario possible.

Scenario 3: A new login walks into an old memory

The tricky one — and the reason we built an extra safety check into login at all.

  1. A customer logs in with a one-time code. Our system is completely confident about who this is. Call them Customer A.
  2. The browser still has an old session remembered by Shopify — maybe from an earlier visit, maybe from someone else who used this browser before.
  3. Shopify reuses that old session instead of asking us again. Normally it would come back and ask who this is. Here it already has something on file, so it skips that step and hands back whatever identity that old session belonged to. That might be Customer A again. It might genuinely be someone else.
  4. We don't trust that answer blindly. We ask Shopify directly who the session belongs to, and compare that against Customer A — the identity our own OTP already confirmed.

Scenario 3: comparing the OTP identity against Shopify's session identity, and what happens on a match versus a mismatch

If the two match: nothing was actually wrong. The login continues, one extra check slower than usual.

If they don't match: we throw away the session we just created, before it ever reaches the browser. buildShopifyLogoutUrl runs again, silently, to clear the stale session on Shopify's side.

That cleanup is the part that matters. Skip it, and every retry keeps landing on the same wrong customer until Shopify's stale session expires on its own.

Scenario 4: Shopify starts the logout, not us

Every scenario so far starts on our website. This one runs the other way — a customer clicks "Sign out" from inside a screen Shopify controls, most commonly its own checkout.

  • Shopify calls our logout endpoint, and tells us where to send the customer once we're done — the checkout page they were already on.
  • We end our own session and send them straight back. Logged out on both sides, no detour.

Scenario 4: Shopify starts the logout, calls our logout endpoint, and the customer returns to the page they were on

The useful thing to notice: keeping two memories in sync isn't only about us reaching out to Shopify. Shopify reaches out to us too, and our side has to answer properly when it does.

Scenario 5: Shopify starts the login, not us

The mirror of Scenario 4. A customer is in checkout, not signed in, and clicks "Sign in" there. That click goes to Shopify first, not us.

  1. Shopify hands the browser over to us, asking us to identify this person. Nothing sits open on a wire — we just record a pending request, good for ten minutes.
  2. We show our own login page — same phone-and-OTP screen as always. The customer never sees a Shopify login screen, even though Shopify is the one who asked.
  3. Once the code is verified, we answer Shopify. It closes out the login it was holding, and the browser lands back in checkout, signed in.

Scenario 5: Shopify starts the login, waits while our own OTP page verifies the customer, then the customer returns to checkout signed in

The difference from a normal login is who moves first. Same OTP page either way — two different orders of events underneath.

A smaller detail: finding the customer at all

When someone types a phone number, we check our own customer table first. A hit takes a few milliseconds and never touches Shopify. A miss means a real call to Shopify's Admin API — a few hundred milliseconds — and we cache that answer so the next login for that person is a hit.

No bulk data migration was needed. All 612,884 customers already lived in Shopify. Instead of copying them across upfront, each one fills themselves in the first time they log in — so the share of logins that still need Shopify keeps shrinking on its own.

Customer lookup order: check our own database first, then Shopify on a miss, then write the result back so the next login is fast

Why not just trust Shopify's answer?

It would be simpler. Fewer checks, faster logins, less code.

But that simplicity only holds if the two memories always agree. They don't — not because either system is broken, but because they're genuinely separate, and separate things drift apart over time. A session that outlives a logout. A browser that gets reused. A customer who logs in from checkout, then again from our own site minutes later. Any of these can put Shopify's memory slightly out of step with ours.

Once you accept the two can disagree, the only honest option is to check. Not to distrust Shopify — to confirm, every time, that "the person who just proved themselves to us" and "the person Shopify's session belongs to" are the same person.

Most of the time that check passes instantly, and nobody notices it happened. Occasionally it catches something real. That's exactly the moment it exists for.

The takeaway

We're not fighting Shopify's login behavior, or routing around it. We're going along with it — while keeping one small, quiet check in place for the moments where going along with it blindly would have been the wrong call.

Pratham Bhatia

Pratham Bhatia

SDE-1

Continue reading

All articles →
Our AI SDLC: 8 Stages, 3 Agent Roles, 6 Gates
ai sdlc

Our AI SDLC: 8 Stages, 3 Agent Roles, 6 Gates

The lifecycle we now run every project through at devx: eight stages, three agent roles with different tool permissions, and six checks that fail the build.

Jaimin MalaviyaJul 30 · 18 min
Pyramid, Diamond, Pod
future of work

Pyramid, Diamond, Pod

Learnings from the ground: The pyramid was never the product. Judgment was. And a better amplifier for judgment now exists!

Yash ThakkerJul 12 · 6 min