Supercommerce API Docs
Store API

Auth — Social Sign-In (Google & Apple)

Customer sign-in with Google and Sign in with Apple: the browser redirect flow, the native ID-token flow the mobile apps use, and the two Apple helper routes (Android return URL, authorization-code capture for token revocation).

Customers sign in with Google or Apple through better-auth's social endpoints. There are two flows:

  • Redirect flow (web). The storefront calls POST /auth/sign-in/social without an idToken. The API returns the provider's authorize URL, the browser goes there, and the provider sends it back to /auth/callback/<provider>, which sets the session cookie and redirects to callbackURL.
  • ID-token flow (mobile). The app signs in natively, then posts the provider's identity token to the same endpoint. The API verifies the token against the provider's public keys and returns a session token directly. No browser is involved.

The first sign-in creates the customer, and fires the welcome email and CUSTOMER_REGISTERED. If a customer with the same verified email already exists, the provider account is linked to that customer instead.

Source: api-modules/auth/src/auth.config.ts (providers), api-modules/auth/src/lib/apple/, api-modules/auth/src/controllers/store-apple-auth.controller.ts.

Each provider is on only when its credentials are set in the API's environment. For Apple, the setup is in the Sign in with Apple guide. A provider that is off returns 404 from the endpoints below.


Conventions

Authentication

EndpointAuth
GET /store/auth/sign-in-methodsnone (public)
POST /auth/sign-in/socialnone (public — creates session)
GET/POST /auth/callback/:providernone (the provider redirects here)
POST /store/auth/apple/android-callbacknone (Apple posts here)
POST /store/auth/apple/authorization-codecustomer session (Authorization: Bearer <token> or cookie)

/auth/* routes are better-auth's own and are not wrapped in the { data, message, statusCode } envelope. Their errors look like { "message": "...", "code": "INVALID_TOKEN" }. /store/auth/* routes use the standard envelope and error shape.


Which buttons to show

GET /store/auth/sign-in-methods

The sign-in options to render, per platform. A method is listed when the operator enabled it for that platform (Settings → Customer Sign-in, admin.sign_in.enabled_methods) and this deployment has what it needs to serve it there. For example, Apple on Android also needs APPLE_ANDROID_PACKAGE. The web storefront reads web; the apps read android or ios. Render a button only for what your platform's list contains, in the order given.

200:

{
  "data": {
    "web": ["email", "google", "apple"],
    "android": ["email", "google"],
    "ios": ["email", "google", "apple"]
  },
  "message": "Success",
  "statusCode": 200
}

Methods are drawn from email, google, apple and phone. phone is never listed until an SMS provider delivers OTPs.

This decides what customers see. It does not disable a method at the API, because staff sign in through the same endpoints. The admin feed behind the setting is documented in Sign-in Methods.


Sign in — redirect flow (web)

POST /auth/sign-in/social

{
  "provider": "apple",            // or "google"
  "callbackURL": "https://shop.example.com/account",   // absolute; must be a trusted origin
  "errorCallbackURL": "https://shop.example.com/?auth_error=1"  // optional
}

200:

{ "url": "https://appleid.apple.com/auth/authorize?...", "redirect": true }

Send the browser to url. The better-auth client does this for you: authClient.signIn.social({ provider: "apple", callbackURL }).

When sign-in fails, the provider callback redirects to errorCallbackURL, or to better-auth's /auth/error page if none was given, with ?error=<code> (for example state_mismatch or invalid_code).

Apple's callback

Apple returns with a cross-site form POST (response_mode=form_post). That request carries no SameSite=Lax cookies, so POST /auth/callback/apple redirects to a GET of the same callback, and the GET completes sign-in with the state cookie present.

When the frontends reach /auth through their own origin (OAUTH_CALLBACK_BASE_URL is set), the POST lands on the proxy and is redirected to the proxied callback, so the state cookie still matches.

Apple sends the customer's name (in the user form field) only on the first authorization. It is stored on the account then and never sent again.


Sign in — ID-token flow (mobile)

POST /auth/sign-in/social

{
  "provider": "apple",            // or "google"
  "disableRedirect": true,
  "idToken": {
    "token": "<identity token JWT>",
    "nonce": "<RAW nonce>",       // Apple: the un-hashed value; the SHA-256 went to Apple
    "accessToken": "<optional>",  // Google only
    "user": {                     // optional hint; Apple sends the name only on first sign-in
      "email": "jane@example.com",
      "name": { "firstName": "Jane", "lastName": "Doe" }
    }
  }
}

200:

{
  "redirect": false,
  "token": "<session token>",     // send as Authorization: Bearer <token>
  "user": { "id": "...", "email": "...", "name": "...", "emailVerified": true, ... }
}

Which token audiences the API accepts:

ProviderAccepted aud
GoogleGOOGLE_CLIENT_ID and every id in GOOGLE_CLIENT_IDS
Applethe Services ID (APPLE_CLIENT_ID: web and Android tokens) and every bundle id in APPLE_APP_BUNDLE_IDENTIFIER (native iOS tokens)

If the request carries a guest (anonymous) session, that guest's cart, orders and addresses move to the signed-in customer.

Errors (401 unless noted):

codeMeaning
INVALID_TOKENSignature, issuer, audience, age (> 1 h) or nonce check failed.
USER_EMAIL_NOT_FOUNDThe token has no email (the customer declined the email scope).
OAUTH_LINK_ERRORAn account with this email exists but the provider did not mark the email as verified, so it was not linked.
PROVIDER_NOT_FOUND (404)That provider is not configured on this deployment.

Apple helper routes

POST /store/auth/apple/android-callback

The Return URL for Sign in with Apple on Android. Android has no native Apple sign-in, so the sign_in_with_apple Flutter plugin opens Apple's web flow, and Apple posts the result here. The response is a 303 to

intent://callback?code=…&id_token=…&state=…&user=…#Intent;package=<APPLE_ANDROID_PACKAGE>;scheme=signinwithapple;end

which hands the result back to the app. The app then signs in with the ID-token flow above. Apps never call this route themselves.

It returns 404 when Apple or APPLE_ANDROID_PACKAGE is not configured.

POST /store/auth/apple/authorization-code

Apple requires an app that offers Sign in with Apple and account deletion to revoke the customer's Apple tokens when they delete their account (App Store Review Guideline 5.1.1(v)). The native flow returns only an identity token, which cannot be revoked. So right after signing in, the app sends the one-time authorizationCode, and the API exchanges it for a refresh token and stores it on the customer's Apple account.

{
  "authorizationCode": "<from the Apple credential>",   // single use, valid 5 minutes
  "platform": "ios"                                       // "ios" (default) | "android"
}

204 No Content on success.

StatusReason
400Apple rejected the code (expired, already used, wrong client), or the code belongs to a different Apple ID than the signed-in account.
401No session.
404Apple (or the platform's client) is not configured, or the signed-in customer has no Apple account.

Token revocation

Whenever an Apple-linked account is removed, its stored refresh token (or, failing that, its access token) is revoked at https://appleid.apple.com/auth/revoke. This covers:

  • self-service deletion (POST /auth/delete-user)
  • admin deletion (DELETE /admin/customers/:id)
  • unlinking Apple from an account

Revocation is best-effort: a failure is logged and never blocks the deletion. Web sign-ins store their tokens automatically. Native sign-ins have tokens only if the app called /store/auth/apple/authorization-code.


Apple and email

  • A customer can choose Hide My Email. Their email is then a …@privaterelay.appleid.com address, and mail to it reaches them only if the operator registered the sending domain with Apple (see the guide).
  • The name can be empty. Apple sends it once, so a customer whose first Apple sign-in did not capture it gets name: "" until they edit their profile.

On this page