> ## Documentation Index
> Fetch the complete documentation index at: https://help.storetalk.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Developer guide

> Add “Continue with WhatsApp” to a website or app: OpenID Connect endpoints, the StoreTalk SDK button, token verification, mobile apps and Auth.js.

Sign in with WhatsApp is a standard **OpenID Connect** provider using the **authorization code flow with
PKCE**. If you've added “Sign in with Google” before, this works the same way — any OIDC library will do.

You need the **Client ID** (and, for websites with a server, the **Client secret**) from
**Settings → Sign in with WhatsApp**. See [Set up](https://help.storetalk.app/sign-in-with-whatsapp/set-up.md).

## Endpoints

| | URL |
| - | - |
| Issuer | `https://auth.storetalk.app` |
| Discovery | `https://auth.storetalk.app/.well-known/openid-configuration` |
| Authorization | `https://auth.storetalk.app/oauth2/authorize` |
| Token | `https://auth.storetalk.app/oauth2/token` |
| UserInfo | `https://auth.storetalk.app/oauth2/userinfo` |
| Revocation | `https://auth.storetalk.app/oauth2/revoke` |
| End session | `https://auth.storetalk.app/oauth2/logout` |
| JWKS | `https://auth.storetalk.app/.well-known/jwks.json` |

## Scopes and claims

| Scope | Claims |
| - | - |
| `openid` (required) | `sub`, `tid` |
| `phone` | `phone_number` (E.164, e.g. `+919876543210`), `phone_number_verified` (always `true`) |
| `profile` | `name` — only when the business has a real name for the customer |

The ID token also carries `iss`, `aud` (your client ID), `azp`, `iat`, `exp`, `auth_time`, `nonce` and
`amr` (`["otp"]` for a fresh code, `["session"]` for *Continue as*).

> [!TIP]
>
> Use **`sub`** as the customer's account key. It's stable for a number **within your business** and
> different for every business. Use `phone_number` to link a sign-in to an existing account you already
> have for that number.

## The flow

**1. Send the browser to the authorization endpoint**

```
https://auth.storetalk.app/oauth2/authorize
  ?client_id=stl_…
  &redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fcallback
  &response_type=code
  &scope=openid%20phone%20profile
  &state=<random>
  &nonce=<random>
  &code_challenge=<BASE64URL(SHA256(code_verifier))>
  &code_challenge_method=S256
```

**PKCE (`S256`) is required for every client**, including websites with a server.
Optional: `prompt=login` always asks for a fresh code; `login_hint=+919876543210` (E.164) pre-fills
the number if its country is allowed for your website.

**2. Handle the callback**

The customer returns to your `redirect_uri` with `?code=…&state=…`. Check `state` matches what you sent.
On failure you get `?error=…&error_description=…&state=…` instead (see [Errors](#errors)).

**3. Exchange the code (within 60 seconds)**

```bash theme={null}
curl -X POST https://auth.storetalk.app/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=CODE \
  -d redirect_uri=https://example.com/auth/callback \
  -d code_verifier=VERIFIER \
  -d client_id=stl_… \
  -d client_secret=sts_…
```

Websites with a server authenticate with `client_secret` (in the body or as HTTP Basic). Browser and
mobile apps send only `client_id` — PKCE proves it's them. Codes work **once** and expire after **60 seconds**.

```json theme={null}
{
  "access_token": "eyJ…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "eyJ…",
  "scope": "openid phone profile"
}
```

**4. Verify the ID token**

Check the RS256 signature against the JWKS, and that `iss` is `https://auth.storetalk.app`, `aud` is
your client ID, `exp` is in the future and `nonce` matches. Then sign the customer in to your own
session.

| Token | Lifetime |
| - | - |
| Authorization code | 60 seconds, single use |
| ID token | 10 minutes |
| Access token (for UserInfo) | 1 hour |

There are no refresh tokens: sign-ins are quick, and customers who come back get **Continue as …** without a code.

## Using the StoreTalk SDK

The `@citymapia-llc/storetalk-app-sdk` package (version **1.19.0+**) includes a ready-made button and
helpers that handle PKCE, state, nonce and token verification. They run in browsers, Node 18+ and edge
runtimes.

> [!NOTE]
>
> The SDK is published to GitHub Packages. Add `@citymapia-llc:registry=https://npm.pkg.github.com` to your
> `.npmrc` with a GitHub token that can read packages. You don't need the SDK — any OIDC library works.

### 1. The button (React)

```tsx theme={null}
import { WhatsAppLoginButton } from '@citymapia-llc/storetalk-app-sdk/react';

export function SignIn() {
  return (
    <WhatsAppLoginButton
      clientId="stl_…"
      redirectUri="https://example.com/auth/callback"
      // locale="hi" | "ml" | "ta"   variant="outline"
    />
  );
}
```

Pass `loginHint="+919876543210"` to pre-fill the number, or `forceLogin` to always ask for a fresh code.
`locale` sets the label (*Continue with WhatsApp* in English, Hindi, Malayalam or Tamil); `variant="outline"`
gives a white button that sits well next to Google/Apple buttons. Not using React? Call
`startWhatsAppLogin({ clientId, redirectUri })` and send the browser to the returned `url`.

### 2. The callback page (browser)

```ts theme={null}
import { completeWhatsAppLogin } from '@citymapia-llc/storetalk-app-sdk';

// On https://example.com/auth/callback
const { code, codeVerifier, nonce } = completeWhatsAppLogin(window.location.href);
await fetch('/api/auth/whatsapp', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ code, codeVerifier, nonce }),
});
```

`completeWhatsAppLogin` throws a `WhatsAppLoginError` if the customer cancelled (`access_denied`) or the
sign-in can't be matched. Each sign-in can be completed only once.

### 3. Your server

```ts theme={null}
import { exchangeWhatsAppLoginCode, verifyWhatsAppIdToken } from '@citymapia-llc/storetalk-app-sdk';

export async function POST(req: Request) {
  const { code, codeVerifier, nonce } = await req.json();
  const tokens = await exchangeWhatsAppLoginCode({
    clientId: process.env.STORETALK_CLIENT_ID!,
    clientSecret: process.env.STORETALK_CLIENT_SECRET, // omit for browser/mobile apps
    code,
    codeVerifier,
    redirectUri: 'https://example.com/auth/callback',
  });
  const user = await verifyWhatsAppIdToken(tokens.idToken, {
    clientId: process.env.STORETALK_CLIENT_ID!,
    nonce,
  });
  // user = { sub, phoneNumber, phoneNumberVerified, name?, tenantId }
  // → find or create your account by user.sub, then start your own session
}
```

`verifyWhatsAppIdToken` checks the signature (caching the JWKS for an hour and refetching when keys
rotate), issuer, audience, expiry and nonce. `getWhatsAppUserInfo(accessToken)` calls the UserInfo endpoint.

## Auth.js / NextAuth

```ts theme={null}
// auth.ts
import NextAuth from 'next-auth';

export const { handlers, auth, signIn } = NextAuth({
  providers: [
    {
      id: 'whatsapp',
      name: 'WhatsApp',
      type: 'oidc',
      issuer: 'https://auth.storetalk.app',
      clientId: process.env.STORETALK_CLIENT_ID,
      clientSecret: process.env.STORETALK_CLIENT_SECRET,
      authorization: { params: { scope: 'openid phone profile' } },
      checks: ['pkce', 'state', 'nonce'],
      profile(claims) {
        return { id: claims.sub, name: claims.name ?? claims.phone_number, phone: claims.phone_number };
      },
    },
  ],
});
```

Register `https://your-site.com/api/auth/callback/whatsapp` as the redirect URI in Settings.

## Other platforms

* **WordPress** — use an OpenID Connect client plugin. Enter the client ID and secret, scope
  `openid phone profile`, and the endpoints above (or the discovery URL if the plugin supports it).
  Enable PKCE.
* **Any server language** — use your language's OIDC client with the discovery URL. Choose the
  *authorization code* flow with *PKCE (S256)*.

## Mobile apps

Register a **Browser or mobile app** client (no secret).

* Open the sign-in page in the system browser sheet — **`ASWebAuthenticationSession`** on iOS,
  **Custom Tabs** on Android. Don't use an embedded WebView: the *Continue as* session isn't shared, and
  sign-in pages inside WebViews are a common phishing pattern.
* Prefer a verified **app link** as the redirect URI (`https://example.com/auth/callback`, verified with
  `apple-app-site-association` / `assetlinks.json`). A custom scheme such as `com.example.app:/oauth` also
  works, but another app could claim it.
* **AppAuth** for iOS and Android works out of the box with the discovery URL.

> [!NOTE]
>
> Apple requires iOS apps that offer third-party sign-in (Google, WhatsApp…) to also offer **Sign in with
> Apple** or an equivalent privacy-focused option (App Store guideline 4.8).

## Browser apps (no server)

Browser apps can exchange the code directly from the page: add your site under **Allowed origins** and call
the token endpoint with a form POST (no `client_secret`). Call the **UserInfo** endpoint from a server, not
the browser — or simply read the claims from the verified ID token.

## Signing out

Your website ends its own session as usual. To also end the *Continue as* session on that browser, send
it to:

```
https://auth.storetalk.app/oauth2/logout
  ?client_id=stl_…
  &post_logout_redirect_uri=https%3A%2F%2Fexample.com%2Fsigned-out
  &state=<optional>
```

The customer comes back to `post_logout_redirect_uri` (with your `state`) only if it's one of the
**Sign-out return addresses** registered in Settings; otherwise they see a StoreTalk confirmation.

To revoke an access token, `POST` `token`, `client_id` (and `client_secret` for websites with a server) to
the revocation endpoint. It always returns `200`.

## Errors

Errors on your redirect URI (`?error=…`):

| `error` | Meaning |
| - | - |
| `access_denied` | The customer tapped **Cancel** on the consent screen. |
| `unauthorized_client` | The business can't offer Sign in with WhatsApp right now (own WhatsApp account not connected, plan not active, or this website is turned off). |
| `invalid_request` | Missing or wrong PKCE (`code_challenge` with `code_challenge_method=S256`). |
| `invalid_scope` | The `openid` scope is missing. |
| `unsupported_response_type` | Only `response_type=code` is supported. |

If the client ID or redirect URI is wrong, the customer sees an error on the StoreTalk page and is **not**
redirected — so a mistyped redirect URI can't send customers elsewhere.

Token endpoint errors (JSON, RFC 6749):

| `error` | Meaning |
| - | - |
| `invalid_client` | Unknown or turned-off client, wrong secret, or an origin that isn't allowed. |
| `invalid_grant` | The code is wrong, expired, already used, or doesn't match the `redirect_uri` / `code_verifier`. |
| `unsupported_grant_type` | Only `authorization_code` is supported. |

## Testing

* Add `http://localhost:3000/…` as a redirect URI while developing.
* The **Codes per day** limit and allowed countries apply during testing too.
* Each code you request is a real WhatsApp message charged to the business's WhatsApp account.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.