Single Sign-On Deep Link

This guide explains how to integrate External Auth SSO into your whitelabel project. Your backend requests a one-time magic login link from our API using a signed payload. When the user is redirected to that link, they are instantly authenticated and sent to your chosen destination page. The document covers required setup, request/response formats, signature generation, common errors, testing steps, and a ready-to-use Postman collection.

1. Before You Start — Required PBO Settings

Enable SSO in your PBO dashboard:

  1. Go to Configuration
  2. Enable Customer Login
  3. Enable SSO
  4. Add your SSO Redirect URL (this is the fallback page for expired or invalid magic links)

Your SSO flow will not work without these settings.


2. How the SSO Flow Works

The flow is simple and handles both signup and login automatically:

  1. Your system requests a magic login link from our API with user information.
  2. We check if the user exists:
    • New user: Creates account automatically
    • Existing user: Authenticates and updates profile if needed
  3. We return a magic login link.
  4. You redirect the user to it.
  5. The user is automatically logged in and sent to your chosen page.

No separate signup/login endpoints needed - the same request works for both scenarios!


3. Step-by-Step Workflow for Integration

Step 1 — Your backend calls our API

POST https://{wl.domain.com}/v1/guest/auth/external-auth

Request Body Fields

Required Fields:

  • firstName (string): User's first name
  • externalUserId (string): Your internal user identifier
  • timestamp (integer): Unix timestamp (seconds)
  • signature (string): HMAC-SHA256 signature

Optional Fields:

  • lastName (string): User's last name

Identifier Fields (at least one required):

  • email (string, optional): User's email address (must be valid email if provided)
  • phoneNo (string, optional): User's phone number with country code (e.g., "+1234567890")

Optional Fields:

  • redirectUrl (string): Destination URL after login
  • country (string): User's country code (see supported countries below)
  • language (string): User's preferred language code (see supported languages below)
  • currency (string): User's preferred currency code (see supported currencies below)

Supported Country Codes

The following ISO 3166-1 alpha-2 country codes are supported:

AD, AE, AF, AG, AI, AL, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BL, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BY, BZ, CA, CC, CD, CF, CG, CH, CI, CK, CL, CM, CN, CO, CR, CU, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, ET, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IQ, IR, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KP, KR, KW, KY, KZ, LA, LB, LC, LI, LK, LR, LS, LT, LU, LV, LY, MA, MC, MD, ME, MF, MG, MH, MK, ML, MM, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NI, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PS, PT, PW, PY, QA, RE, RO, RS, RU, RW, SA, SB, SC, SD, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SO, SR, SS, ST, SV, SX, SY, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VC, VE, VG, VI, VN, VU, WF, WS, YE, YT, ZA, ZM, ZW

Examples:

  • US - United States
  • GB - United Kingdom
  • IN - India
  • CA - Canada
  • AU - Australia
  • DE - Germany
  • FR - France
  • JP - Japan
  • CN - China
  • AE - United Arab Emirates

Supported Language Codes

The following language codes are supported:

en, fr, ru, it, nl, es, tr, de, ar, pt, el, ro, pl, cs, hu, lv, ja, da, nb, lt, sk, hr, sv, bg, et, ca, fi, sl, zh, uk, ko, ms

Examples:

  • en - English
  • es - Spanish
  • fr - French
  • de - German
  • ar - Arabic
  • zh - Chinese
  • ja - Japanese

Supported Currency Codes

The following currency codes are supported:

AED, AMD, ARS, AUD, AZN, BGN, BHD, BRL, CAD, CHF, CLP, CNY, COP, CVE, CZK, DKK, DOP, EGP, EUR, FJD, GBP, GEL, GHS, HKD, HUF, IDR, ILS, INR, ISK, JOD, JPY, KRW, KWD, KZT, LKR, MAD, MNT, MUR, MXN, MYR, NGN, NOK, NZD, OMR, PEN, PHP, PKR, PLN, QAR, RON, RUB, SAR, SEK, SGD, THB, TRY, TWD, UAH, USD, VND, XOF, XPF, ZAR

Examples:

  • USD - US Dollar
  • EUR - Euro
  • GBP - Pound Sterling
  • INR - Indian Rupee
  • JPY - Japanese Yen
  • CNY - Chinese Yuan
  • AED - UAE Dirham

Signup vs Login Behavior

The endpoint automatically handles both scenarios:

  • New User (Signup):

    • User doesn't exist in the system
    • Creates new account with provided information
    • Returns magic login link
  • Existing User (Login):

    • User found by email or phoneNo
    • Updates profile information if provided
    • Returns magic login link

You don't need to check if the user exists - just send the request and the endpoint handles it!

Request Body Examples

Email-Only Authentication (Signup or Login)

{
  "email": "[email protected]",
  "firstName": "First",
  "lastName": "Last",
  "externalUserId": "CLIENT-123",
  "timestamp": 1763466236,
  "signature": "HMAC_SIGNATURE_HERE",
  "redirectUrl": "/hotels",
  "country": "US",
  "language": "en",
  "currency": "USD"
}

Phone-Only Authentication (Signup or Login)

{
  "phoneNo": "+1234567890",
  "firstName": "Jane",
  "lastName": "Smith",
  "externalUserId": "CLIENT-456",
  "timestamp": 1763466236,
  "signature": "HMAC_SIGNATURE_HERE",
  "redirectUrl": "/hotels",
  "country": "US",
  "language": "en",
  "currency": "USD"
}

Email + Phone Authentication (Signup or Login)

{
  "email": "[email protected]",
  "phoneNo": "+1234567890",
  "firstName": "Bob",
  "lastName": "Johnson",
  "externalUserId": "CLIENT-789",
  "timestamp": 1763466236,
  "signature": "HMAC_SIGNATURE_HERE",
  "redirectUrl": "/hotels",
  "country": "US",
  "language": "en",
  "currency": "USD"
}

Authentication Without LastName (Optional)

{
  "phoneNo": "+1234567890",
  "firstName": "Alex",
  "externalUserId": "CLIENT-999",
  "timestamp": 1763466236,
  "signature": "HMAC_SIGNATURE_HERE",
  "redirectUrl": "/hotels",
  "country": "US",
  "language": "en",
  "currency": "USD"
}

Note: lastName is optional. You can omit it if not available.

Required Headers

Content-Type: application/json
X-Liteapi-Key: <your LiteAPI key>

Signature Creation

The signature is created using either email or phoneNo as the identifier. If both are provided, email takes precedence for backward compatibility.

With Email:

identifier = email.toLowerCase()
payload = identifier + ":" + timestamp + ":" + externalUserId
signature = HMAC_SHA256(liteApiKey, payload)

With Phone Number Only:

identifier = phoneNo.trim()
payload = identifier + ":" + timestamp + ":" + externalUserId
signature = HMAC_SHA256(liteApiKey, payload)

Important Notes:

  • The timestamp must be within ±5 minutes of current time.
  • Email is automatically lowercased before signature generation.
  • Phone number should be trimmed (no leading/trailing spaces).
  • If both email and phoneNo are provided, use email for the signature.

Step 2 — We return a Magic Login URL

Example response:

{
  "loginUrl": "https://project-domain.example/auth/magic-login?token=ABC123&redirectUrl=https%3A%2F%2Fproject-domain.example%2Fhotels"
}

The magic link:

  • Logs the user in immediately
  • Is valid once only
  • Expires in ~30 minutes
  • Contains all necessary session context

Step 3 — Redirect the User to loginUrl

Perform a standard 302 redirect:

302 → https://project-domain.example/auth/magic-login?token=ABC123...

Do not modify the URL.


Step 4 — User Logs In and Gets Forwarded

After opening the magic link:

  • Token is validated
  • User is authenticated (created if new, logged in if existing)
  • User is forwarded to your original redirect URL

Note: The endpoint handles both signup and login automatically. You don't need separate endpoints or logic to check if a user exists.

Successful Redirect Example

https://project-domain.example/hotels?token=<session>&magicLogin=true

Invalid / Used Token Redirect Example

https://your-site.example/sso-error?error=TOKEN_ALREADY_USED&magicLogin=true

4. Complete Examples (Start → End)

The endpoint automatically handles both signup and login. The same request format works for both scenarios.

Example 1: Email-Only Authentication (New User - Signup)

Your Request

{
  "email": "[email protected]",
  "firstName": "Sarah",
  "lastName": "Smith",
  "externalUserId": "USER-001",
  "timestamp": 1763466236,
  "signature": "ab28fd91c0...",
  "redirectUrl": "https://travel-brand.example/hotels",
  "country": "US",
  "language": "en",
  "currency": "USD"
}

Our Response

{
  "loginUrl": "https://travel-brand.example/auth/magic-login?token=Zxcv098765..."
}

Browser Redirect

https://travel-brand.example/auth/magic-login?token=Zxcv098765...

Final Landing Page

https://travel-brand.example/hotels?token=<session>&magicLogin=true

Example 2: Phone-Only Authentication (Existing User - Login)

Your Request

{
  "phoneNo": "+14155551234",
  "firstName": "John",
  "lastName": "Doe",
  "externalUserId": "USER-002",
  "timestamp": 1763466236,
  "signature": "cd39ef02d1...",
  "redirectUrl": "https://travel-brand.example/hotels",
  "country": "US",
  "language": "en",
  "currency": "USD"
}

Our Response

{
  "loginUrl": "https://travel-brand.example/auth/magic-login?token=Yxcv098765..."
}

Example 3: Email + Phone Authentication (New User - Signup)

Your Request

{
  "email": "[email protected]",
  "phoneNo": "+14155555678",
  "firstName": "Bob",
  "lastName": "Johnson",
  "externalUserId": "USER-003",
  "timestamp": 1763466236,
  "signature": "ef40fg13e2...",
  "redirectUrl": "https://travel-brand.example/hotels",
  "country": "US",
  "language": "en",
  "currency": "USD"
}

Note: When both email and phoneNo are provided, use email for signature generation.

Signup/Login Behavior:

  • The endpoint automatically handles both new users (signup) and existing users (login)
  • Same request format works for both scenarios - no need to check if user exists
  • New users: Account is created automatically
  • Existing users: Authenticated and profile updated if needed

5. Common Errors

ErrorMeaningFix
INVALID_SIGNATURESignature doesn't matchEnsure payload is exactly {identifier}:{timestamp}:{externalUserId} where identifier is email (lowercased) or phoneNo (trimmed)
EXPIRED_REQUESTTimestamp older than 5 minCreate new timestamp + signature
INVALID_INPUTMissing or invalid valuesCheck spelling and required fields. Ensure at least email or phoneNo is provided
TOKEN_ALREADY_USEDMagic link opened twiceGenerate a fresh link
Missing SSO settingsSSO disabled or fallback missingConfirm PBO settings

6. Magic Login Link Behavior

When visiting loginUrl:

  • Token is validated
  • User logs into the whitelabel project
  • User is redirected to:
{redirectUrl}?token=<session>&magicLogin=true

If token is invalid or reused, user is redirected to:

{ssoReturnUrl}?error=...&magicLogin=true

ssoReturnUrl must be an HTTPS URL that you control.


7. Setup Checklist

  • LiteAPI key (used in header + HMAC secret)
  • Enable SSO
  • Set Fallback redirect URL (e.g., https://your-site.example/sso-error)
  • Allowed domains configured
  • HTTPS used everywhere

8. Testing Tips

Shell Example: Email-Only

ts=$(date +%s)
EMAIL="[email protected]"
IDENTIFIER=$(echo "$EMAIL" | tr '[:upper:]' '[:lower:]')
payload="${IDENTIFIER}:${ts}:CLIENT-123"
signature=$(printf '%s' "$payload" | openssl dgst -sha256 -hmac "$LITEAPI_KEY" | awk '{print $2}')

curl -X POST "https://{wl.domain.com}/v1/guest/auth/external-auth" \
  -H "Content-Type: application/json" \
  -H "X-Liteapi-Key: $LITEAPI_KEY" \
  -d "{\"email\":\"$EMAIL\",\"firstName\":\"First\",\"lastName\":\"Last\",\"externalUserId\":\"CLIENT-123\",\"timestamp\":${ts},\"signature\":\"${signature}\",\"redirectUrl\":\"https://project-domain.example/hotels\",\"country\":\"US\",\"language\":\"en\",\"currency\":\"USD\"}"

Shell Example: Phone-Only

ts=$(date +%s)
PHONE="+1234567890"
IDENTIFIER=$(echo "$PHONE" | xargs)  # trim whitespace
payload="${IDENTIFIER}:${ts}:CLIENT-456"
signature=$(printf '%s' "$payload" | openssl dgst -sha256 -hmac "$LITEAPI_KEY" | awk '{print $2}')

curl -X POST "https://{wl.domain.com}/v1/guest/auth/external-auth" \
  -H "Content-Type: application/json" \
  -H "X-Liteapi-Key: $LITEAPI_KEY" \
  -d "{\"phoneNo\":\"$PHONE\",\"firstName\":\"Jane\",\"lastName\":\"Smith\",\"externalUserId\":\"CLIENT-456\",\"timestamp\":${ts},\"signature\":\"${signature}\",\"redirectUrl\":\"https://project-domain.example/hotels\",\"country\":\"US\",\"language\":\"en\",\"currency\":\"USD\"}"

Shell Example: Email + Phone

ts=$(date +%s)
EMAIL="[email protected]"
PHONE="+1234567890"
# Use email for signature when both are provided
IDENTIFIER=$(echo "$EMAIL" | tr '[:upper:]' '[:lower:]')
payload="${IDENTIFIER}:${ts}:CLIENT-789"
signature=$(printf '%s' "$payload" | openssl dgst -sha256 -hmac "$LITEAPI_KEY" | awk '{print $2}')

curl -X POST "https://{wl.domain.com}/v1/guest/auth/external-auth" \
  -H "Content-Type: application/json" \
  -H "X-Liteapi-Key: $LITEAPI_KEY" \
  -d "{\"email\":\"$EMAIL\",\"phoneNo\":\"$PHONE\",\"firstName\":\"Bob\",\"lastName\":\"Johnson\",\"externalUserId\":\"CLIENT-789\",\"timestamp\":${ts},\"signature\":\"${signature}\",\"redirectUrl\":\"https://project-domain.example/hotels\",\"country\":\"US\",\"language\":\"en\",\"currency\":\"USD\"}"

Open the loginUrl returned in the response.

To test an error:
Open the same link twice → should redirect with TOKEN_ALREADY_USED.


9. Troubleshooting

  • JSON response instead of redirect
    Likely accessing the magic link at the API host instead of project domain.
    Also ensure isSsoActive + ssoReturnUrl are set.

  • Signature mismatch

    • Remove stray spaces; ensure email is lowercased and phoneNo is trimmed
    • Verify you're using the correct identifier (email or phoneNo) in the payload
    • If both email and phoneNo are provided, use email for signature generation
    • Ensure timestamp matches exactly between request and signature generation
  • Token already used
    Request a new magic link.

For help, contact integrations support with:

  • timestamp
  • email (if used)
  • phoneNo (if used)
  • project domain

10. Postman Collection

JSON

{
  "info": {
    "_postman_id": "2858d35b-3368-4c81-8c8c-33f282a6a2e1",
    "name": "External Auth SSO",
    "description": "Sample requests for the /guest/auth/external-auth flow.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_exporter_id": "31030075"
  },
  "item": [
    {
      "name": "Create Magic Login Link",
      "request": {
        "method": "POST",
        "header": [
          { "key": "Content-Type", "value": "application/json" },
          { "key": "X-Liteapi-Key", "value": "{{liteApiKey}}" }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"email\": \"{{email}}\",\n  \"phoneNo\": \"{{phoneNo}}\",\n  \"firstName\": \"{{firstName}}\",\n  \"lastName\": \"{{lastName}}\",\n  \"externalUserId\": \"{{externalUserId}}\",\n  \"timestamp\": {{timestamp}},\n  \"signature\": \"{{signature}}\",\n  \"redirectUrl\": \"{{redirectUrl}}\",\n  \"country\": \"{{country}}\",\n  \"language\": \"{{language}}\",\n  \"currency\": \"{{currency}}\"\n}"
        },
        "url": {
          "raw": "{{apiBaseUrl}}/guest/auth/external-auth",
          "host": ["{{apiBaseUrl}}"],
          "path": ["guest", "auth", "external-auth"]
        },
        "description": "Calls the external-auth endpoint to obtain a magic-login URL."
      },
      "response": []
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Get variables (Postman returns undefined if not set, or empty string if set to empty)",
          "const email = pm.collectionVariables.get('email');",
          "const phoneNo = pm.collectionVariables.get('phoneNo');",
          "const timestamp = Math.floor(Date.now() / 1000);",
          "const externalUserId = pm.collectionVariables.get('externalUserId');",
          "const liteApiKey = pm.collectionVariables.get('liteApiKey');",
          "",
          "// Validate required fields",
          "if (!externalUserId) {",
          "  throw new Error('externalUserId is required');",
          "}",
          "if (!liteApiKey) {",
          "  throw new Error('liteApiKey is required');",
          "}",
          "",
          "// Determine identifier: email takes precedence if both are provided (for backward compatibility)",
          "// Handle empty strings by checking if trimmed value exists",
          "let identifier = '';",
          "if (email && email.trim()) {",
          "  identifier = email.trim().toLowerCase();",
          "} else if (phoneNo && phoneNo.trim()) {",
          "  identifier = phoneNo.trim();",
          "} else {",
          "  throw new Error('Either email or phoneNo must be provided (and not empty)');",
          "}",
          "",
          "// Build payload exactly as server expects: identifier:timestamp:externalUserId",
          "const payload = `${identifier}:${timestamp}:${externalUserId}`;",
          "",
          "// Generate signature using CryptoJS (same as Node.js crypto)",
          "const signature = CryptoJS.HmacSHA256(payload, liteApiKey).toString();",
          "",
          "// Store values for use in request",
          "pm.collectionVariables.set('timestamp', timestamp);",
          "pm.collectionVariables.set('signature', signature);",
          "",
          "// Optional: Log for debugging (remove in production)",
          "console.log('Signature Generation:');",
          "console.log('  Identifier:', identifier);",
          "console.log('  Payload:', payload);",
          "console.log('  Signature:', signature);",
          "console.log('  Timestamp:', timestamp);"
        ]
      }
    },
    { "listen": "test", "script": { "type": "text/javascript", "exec": [""] } }
  ],
  "variable": [
    { "key": "apiBaseUrl", "value": "http://site.wlbl.local/v1" },
    { "key": "liteApiKey", "value": "" },
    { "key": "email", "value": "[email protected]" },
    { "key": "phoneNo", "value": "" },
    { "key": "firstName", "value": "First" },
    { "key": "lastName", "value": "Last" },
    { "key": "externalUserId", "value": "CLIENT-123" },
    { "key": "timestamp", "value": "1763466236" },
    { "key": "signature", "value": "" },
    { "key": "redirectUrl", "value": "https://project-domain.example/hotels" },
    { "key": "country", "value": "US" },
    { "key": "language", "value": "en" },
    { "key": "currency", "value": "USD" }
  ]
}

How to Use the Collection

Step 1 — Save

Save the JSON as:

external-auth-sso.postman_collection.json

Step 2 — Import

  • Open Postman
  • Click Import
  • Select the file

It will appear as External Auth SSO.

Step 3 — Update Required Variables

VariableDescriptionRequired
apiBaseUrlYour API base (e.g., https://wl.domain.com/v1)Yes
liteApiKeyYour LiteAPI keyYes
emailUser email (at least one of email/phoneNo)No*
phoneNoUser phone with country code (e.g., "+1234567890")No*
firstNameUser first nameYes
lastNameUser last nameYes
externalUserIdYour internal user IDYes
redirectUrlUser destination after loginNo
countryUser's country code (e.g., "US", "GB")No
languageUser's preferred language (e.g., "en", "es")No
currencyUser's preferred currency (e.g., "USD", "EUR")No

* At least one of email or phoneNo must be provided

Do not update:

  • timestamp
  • signature

These are auto-generated.

Step 4 — How the Pre-Request Script Works

// Get variables (Postman returns undefined if not set, or empty string if set to empty)
const email = pm.collectionVariables.get('email');
const phoneNo = pm.collectionVariables.get('phoneNo');
const timestamp = Math.floor(Date.now() / 1000);
const externalUserId = pm.collectionVariables.get('externalUserId');
const liteApiKey = pm.collectionVariables.get('liteApiKey');

// Validate required fields
if (!externalUserId) {
  throw new Error('externalUserId is required');
}
if (!liteApiKey) {
  throw new Error('liteApiKey is required');
}

// Determine identifier: email takes precedence if both are provided (for backward compatibility)
// Handle empty strings by checking if trimmed value exists
let identifier = '';
if (email && email.trim()) {
  identifier = email.trim().toLowerCase();
} else if (phoneNo && phoneNo.trim()) {
  identifier = phoneNo.trim();
} else {
  throw new Error('Either email or phoneNo must be provided (and not empty)');
}

// Build payload exactly as server expects: identifier:timestamp:externalUserId
const payload = `${identifier}:${timestamp}:${externalUserId}`;

// Generate signature using CryptoJS (same as Node.js crypto)
const signature = CryptoJS.HmacSHA256(payload, liteApiKey).toString();

// Store values for use in request
pm.collectionVariables.set('timestamp', timestamp);
pm.collectionVariables.set('signature', signature);

// Optional: Log for debugging (remove in production)
console.log('Signature Generation:');
console.log('  Identifier:', identifier);
console.log('  Payload:', payload);
console.log('  Signature:', signature);
console.log('  Timestamp:', timestamp);

It:

  • Creates a new timestamp
  • Determines the identifier (email takes precedence if both are provided)
  • Builds the payload using the identifier
  • Generates the HMAC signature
  • Stores both automatically

Step 5 — Send the Request

Open Create Magic Login Link → click Send.

Look for the field:

loginUrl

Step 6 — Test Login Link

Open the loginUrl in a browser → should auto-log in.

To test an error:
Open the link again → you should see:

TOKEN_ALREADY_USED