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:
- Go to Configuration
- Enable Customer Login
- Enable SSO
- 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:
- Your system requests a magic login link from our API with user information.
- We check if the user exists:
- New user: Creates account automatically
- Existing user: Authenticates and updates profile if needed
- We return a magic login link.
- You redirect the user to it.
- 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 nameexternalUserId(string): Your internal user identifiertimestamp(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 logincountry(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 StatesGB- United KingdomIN- IndiaCA- CanadaAU- AustraliaDE- GermanyFR- FranceJP- JapanCN- ChinaAE- 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- Englishes- Spanishfr- Frenchde- Germanar- Arabiczh- Chineseja- 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 DollarEUR- EuroGBP- Pound SterlingINR- Indian RupeeJPY- Japanese YenCNY- Chinese YuanAED- 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:
lastNameis 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
loginUrlPerform 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
| Error | Meaning | Fix |
|---|---|---|
| INVALID_SIGNATURE | Signature doesn't match | Ensure payload is exactly {identifier}:{timestamp}:{externalUserId} where identifier is email (lowercased) or phoneNo (trimmed) |
| EXPIRED_REQUEST | Timestamp older than 5 min | Create new timestamp + signature |
| INVALID_INPUT | Missing or invalid values | Check spelling and required fields. Ensure at least email or phoneNo is provided |
| TOKEN_ALREADY_USED | Magic link opened twice | Generate a fresh link |
| Missing SSO settings | SSO disabled or fallback missing | Confirm 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 ensureisSsoActive+ssoReturnUrlare 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
| Variable | Description | Required |
|---|---|---|
| apiBaseUrl | Your API base (e.g., https://wl.domain.com/v1) | Yes |
| liteApiKey | Your LiteAPI key | Yes |
| User email (at least one of email/phoneNo) | No* | |
| phoneNo | User phone with country code (e.g., "+1234567890") | No* |
| firstName | User first name | Yes |
| lastName | User last name | Yes |
| externalUserId | Your internal user ID | Yes |
| redirectUrl | User destination after login | No |
| country | User's country code (e.g., "US", "GB") | No |
| language | User's preferred language (e.g., "en", "es") | No |
| currency | User's preferred currency (e.g., "USD", "EUR") | No |
* At least one of email or phoneNo must be provided
Do not update:
timestampsignature
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
Updated 10 days ago