Skip to content

App with no backend (SDK)

@authaction/whatsapp-sdk signs people in with WhatsApp from your app and gives it ordinary AuthAction tokens: an ID token, an access token and a refresh token. It uses PKCE, so the app needs no backend and no client secret, and nothing opens a browser. It works in browsers, React Native and Expo.

Before you start, create the WhatsApp connection and enable it for your application, as in Set it up in the dashboard. You need the application’s Client ID and your tenant domain, <tenant>.<region>.authaction.com.

Terminal window
npm install @authaction/whatsapp-sdk

signIn starts a sign-in, hands you the links to show, waits for the message and returns the tokens.

import { WhatsappAuth } from "@authaction/whatsapp-sdk";
import QRCode from "qrcode";
const auth = new WhatsappAuth({
domain: "<tenant>.<region>.authaction.com",
clientId: "<CLIENT_ID>",
audience: "<API_IDENTIFIER>", // optional: an access token for your API
storage: {
get: (k) => localStorage.getItem(k),
set: (k, v) => localStorage.setItem(k, v),
remove: (k) => localStorage.removeItem(k),
},
});
const tokens = await auth.signIn({
open: async (s) => {
// On a phone, the link opens WhatsApp. On a computer, the person scans
// the QR code with the phone that has WhatsApp and sends from there.
signInLink.href = s.webUrl;
qrImage.src = await QRCode.toDataURL(s.webUrl);
},
});
// tokens.idToken, tokens.accessToken, tokens.refreshToken, tokens.expiresAt
import * as Crypto from "expo-crypto";
import * as SecureStore from "expo-secure-store";
import { AppState, Linking } from "react-native";
import QRCode from "react-native-qrcode-svg";
import { WhatsappAuth } from "@authaction/whatsapp-sdk";
const auth = new WhatsappAuth({
domain: "<tenant>.<region>.authaction.com",
clientId: "<CLIENT_ID>",
randomBytes: (n) => Crypto.getRandomBytes(n), // React Native has no crypto.getRandomValues
storage: {
get: (k) => SecureStore.getItemAsync(k),
set: (k, v) => SecureStore.setItemAsync(k, v),
remove: (k) => SecureStore.deleteItemAsync(k),
},
});
// Check at once when the person comes back from WhatsApp.
AppState.addEventListener("change", (s) => s === "active" && auth.checkNow());
const tokens = await auth.signIn({
open: async (s) => {
if (await Linking.canOpenURL(s.whatsappUrl)) await Linking.openURL(s.whatsappUrl);
else showQr(<QRCode value={s.webUrl} size={220} />); // WhatsApp is on another phone
},
});

To let the app check whether WhatsApp is installed, add whatsapp to LSApplicationQueriesSchemes on iOS, and <queries><package android:name="com.whatsapp" /></queries> to AndroidManifest.xml on Android.

signIn is start, your open, then waitForTokens. Call them separately to show your own waiting screen or a cancel button:

const started = await auth.start(); // { transactionId, whatsappUrl, webUrl, message, expiresAt }
const tokens = await auth.waitForTokens(started, {
signal: abortController.signal, // cancel
intervalMs: 2000,
onStatus: (s) => console.log(s), // "pending" | "verified" | "completed" | "expired"
});

A dropped connection or a server error doesn’t end the wait. The SDK keeps checking until the code runs out, so a weak signal only delays the sign-in.

With storage, the SDK keeps the tokens and refreshes them for you:

const accessToken = await auth.getAccessToken(); // refreshed a minute before it runs out
await auth.signOut(); // revokes the refresh token and forgets the tokens

Refresh tokens rotate, and each refresh grants the application’s Refresh token lifetime again (30 days unless you set it, up to a year).

Errors are WhatsappSignInError with a code:

CodeMeaning
expiredThe 10-minute code ran out. Start again.
abortedYou cancelled the wait with signal.
signed_outThere are no tokens to refresh. Sign in again.
httpAuthAction returned an error.
no_randomNo secure random source. Pass randomBytes.

Your API checks the access token like any AuthAction token: a JWT signed with the keys at https://<tenant>.<region>.authaction.com/.well-known/jwks.json, with azp set to your Client ID and aud to your API identifier. See Backend Integrations for ready-made middleware.

The SDK is JavaScript. A Swift or Kotlin app makes the same three calls itself, and the connection’s Quickstart tab in the dashboard has complete Swift and Kotlin code.

For every sign-in, make a random code_verifier of 43 to 128 characters and keep it in the app. Send only code_challenge = BASE64URL(SHA-256(code_verifier)).

Start the sign-in. No token is needed: the Client ID and the challenge are enough.

Terminal window
curl --request POST \
--url https://<tenant>.<region>.authaction.com/api/v1/whatsapp-public/transaction \
--header 'content-type: application/json' \
--data '{
"client_id": "<CLIENT_ID>",
"code_challenge": "<CODE_CHALLENGE>",
"code_challenge_method": "S256"
}'
{
"transactionId": "KQc0LuRcYsvlRNXnVS3keUwYJiuNhUoX",
"message": "Sign in to <app>. Send this message as it is.\n\n2CK4…",
"whatsappUrl": "whatsapp://send?phone=…&text=…",
"webUrl": "https://wa.me/…?text=…",
"expiresAt": "2026-10-30T10:10:00.000Z"
}

Open WhatsApp and wait. Open whatsappUrl, or show webUrl as a QR code, then poll every 2 seconds:

Terminal window
curl 'https://<tenant>.<region>.authaction.com/api/v1/whatsapp-public/transaction/<TRANSACTION_ID>/status?client_id=<CLIENT_ID>'
{ "status": "pending" }
{ "status": "verified", "nonce": "…" }
{ "status": "expired" }

Redeem the nonce with the code_verifier. audience is optional.

Terminal window
curl --request POST \
--url https://<tenant>.<region>.authaction.com/oauth2/token \
--header 'content-type: application/json' \
--data '{
"grant_type": "urn:authaction:params:oauth:grant-type:whatsapp",
"client_id": "<CLIENT_ID>",
"nonce": "<NONCE>",
"code_verifier": "<CODE_VERIFIER>",
"audience": "<API_IDENTIFIER>"
}'
{
"id_token": "eyJhbGciOiJSUzI1NiIs…",
"access_token": "eyJhbGciOiJSUzI1NiIs…",
"refresh_token": "3f9c1e…",
"expires_in": 86400,
"refresh_token_expires_in": 2592000,
"token_type": "Bearer"
}

Only the app holding the code_verifier can redeem the nonce, so the nonce is useless to anyone else who sees the status. Refresh with the Client ID and no secret:

Terminal window
curl --request POST \
--url https://<tenant>.<region>.authaction.com/oauth2/token \
--header 'content-type: application/json' \
--data '{
"grant_type": "refresh_token",
"client_id": "<CLIENT_ID>",
"refresh_token": "<REFRESH_TOKEN>"
}'