V2.0 Widgets - Drop-in Authentication
V2.0 Widgets provide a fully-managed authentication interface that runs in an iframe with zero frontend complexity. Unlike the standard API (v1.0), you get a complete OTP flow with built-in security and fraud detection.
V2.0 Widgets Enable PPSA Eligibility
Pay-Per-Successful-Authentication (PPSA) is only available with V2.0 Widgets for qualifying startups. Requirements will be published by end of December 2025. Eligible users can save 20-40% by only paying for verified users, not failed attempts or bot traffic.
Why Choose V2.0 Widgets
| Feature | V2.0 Widgets | V1.0 Standard API |
|---|---|---|
| Integration Time | 15 minutes | 20+ minutes |
| Frontend Code | 5 lines | 100+ lines |
| Backend Code | 1 API call | 3 API calls |
| UI/UX | Fully managed by Akedly | You build everything |
| Pricing Model | PPSA eligible (requirements apply) | PPM (Pay per message) always |
| Captcha Protection | Built-in & FREE | Not included |
| Device Fingerprinting | Built-in | Limited |
| Circuit Breaker | Built-in | Not included |
| Custom Branding | Logo, colors, company name | No customization |
How V2.0 Widgets Work
The authentication flow is simple and secure:
Your User Initiates Authentication
User clicks "Login" or "Verify Phone" in your application.
Your Backend Creates Attempt
Your backend calls Akedly API with HMAC signature:
POST /api/v1/widget-sdk/create-attemptAkedly Returns Transaction URL
Response includes:
- Name
attemptId- Type
- string
- Description
Unique attempt identifier
- Name
iframeUrl- Type
- string
- Description
URL to open in iframe for authentication
- Name
expiresAt- Type
- string
- Description
ISO 8601 timestamp when attempt expires
Your Frontend Opens Widget
Display the authentication widget in an iframe OR open a Webview/new tab for the user:
<iframe src={iframeUrl} />Your User Completes Authentication
Inside the widget (managed by Akedly):
- •Device fingerprinting and security checks
- •Bot protection
- •Rate limiting
- •Fraud detection
- •OTP delivery (WhatsApp → Telegram → SMS → Email) (depending on your pipeline setup)
- •User enters OTP code
Receive Verification Result 🎉
Akedly redirects user to your callback URL & sends webhook to your backend with verification status.
The entire authentication process (captcha, OTP, verification) is handled inside the widget. You only need to create the attempt and handle the success callback.
Step 1: Create Widget in Dashboard
Before integrating, create a widget in your Akedly dashboard.
Dashboard-Only Configuration
Widget creation is done entirely through the dashboard UI. There is no API for creating widgets. This ensures proper security configuration and prevents unauthorized widget creation.
Dashboard Navigation
- Log in to your Akedly dashboard at https://app.akedly.io
- Navigate to Widgets in the left sidebar
- Click Create New Widget
Basic Settings
- Name
Widget Name- Type
- string
- Description
Internal name for your reference (e.g., "Production Login Widget")
- Name
Description- Type
- string
- Description
Optional description of where this widget is used
Branding Customization:
- Name
Company Logo- Type
- file
- Description
Upload your logo (displayed at top of widget)
- Name
Company Name- Type
- string
- Description
Your company name (shown in widget header)
- Name
Primary Color- Type
- string
- Description
Main brand color for buttons and accents
- Name
Secondary Color- Type
- string
- Description
Background and secondary UI elements
Callback URLs
V2.0 Widgets support two types of callbacks. See Pipeline Setup for detailed configuration, or navigate to Pipelines → Select your pipeline → Callback URLs.
Frontend Callback URL (Must)
Where to redirect the user after authentication completes.
Example:
https://yourapp.com/auth/callback
Success Redirect:
https://yourapp.com/auth/callback?status=success&transactionId=mtx_abc123&attemptId=attempt_xyz789×tamp=2025-01-16T12:00:00Z
Failure Redirect:
https://yourapp.com/auth/callback?status=failed&error=INVALID_OTP&transactionId=mtx_abc123&attemptId=attempt_xyz789×tamp=2025-01-16T12:00:00.000Z
Backend Callback URL (Must)
Webhook endpoint to receive verification events. Akedly sends a POST request with complete verification details immediately after successful or failed verification.
Example:
https://yourapp.com/api/webhooks/akedly
Frontend Redirect Query Parameters:
- Name
status- Type
- string
- Description
"success" or "failed"
- Name
transactionId- Type
- string
- Description
The transaction ID from the verification flow
- Name
attemptId- Type
- string
- Description
The original attemptId you created
- Name
timestamp- Type
- string
- Description
ISO 8601 timestamp of when authentication completed
- Name
error- Type
- string
- Description
Error code (only present if status=failed)
Security Settings
Configure captcha, rate limiting, and fraud protection in the widget security settings panel.
Captcha Settings
Protect your widget from automated attacks with built-in captcha verification.
- Name
Enable Captcha- Type
- boolean
- Description
Always enabled by default. Captcha verification is mandatory for all widget interactions and cannot be disabled.
- Name
Require Cloudflare Turnstile- Type
- boolean
- Description
Always enabled. Cloudflare Turnstile is required to protect your quotas and widgets from bot spam. This setting cannot be turned off to ensure maximum security against automated abuse.
Rate Limiting
Control authentication and OTP request limits to protect your widget from abuse. Rate limits are configured across three dimensions: per phone number, per device ID (fingerprinting), and per widget.
AI-Powered Recommendations: If you have sufficient historical data, use the "Get Recommendations" feature to analyze your usage patterns and suggest optimal rate limits that balance security with user experience.
Default values are suitable for 95% of use cases. Only adjust if you have specific requirements. If you expect bursty traffic, focus on adjusting the per-minute value—the per-hour and per-day values will auto-calculate based on safe ratios.
Widget Attempts
Widget attempts track iframe loads and authentication attempts, regardless of whether they pass captcha or fingerprinting. A failed captcha still counts as an attempt, even if no OTP is sent.
Per Phone Number
- Name
Per Minute- Type
- number
- Description
Default: 2. Maximum widget attempts per phone number per minute.
- Name
Per Hour- Type
- number
- Description
Default: 6. Maximum widget attempts per phone number per hour.
- Name
Per Day- Type
- number
- Description
Default: 10. Maximum widget attempts per phone number per day.
Per Device ID
- Name
Per Minute- Type
- number
- Description
Default: 2. Maximum widget attempts per device fingerprint per minute.
- Name
Per Hour- Type
- number
- Description
Default: 6. Maximum widget attempts per device fingerprint per hour.
- Name
Per Day- Type
- number
- Description
Default: 10. Maximum widget attempts per device fingerprint per day.
Per Widget
- Name
Per Minute- Type
- number
- Description
Default: 25. Total widget attempts allowed per minute across all users.
- Name
Per Hour- Type
- number
- Description
Default: 125. Auto-calculated based on per-minute value (5x ratio).
- Name
Per Day- Type
- number
- Description
Default: 2500. Auto-calculated based on per-minute value (100x ratio).
OTP Requests
OTP requests track actual One-Time Password deliveries. These limits apply only when an OTP is successfully sent to the user.
Per Phone Number
- Name
Per Minute- Type
- number
- Description
Default: 1. Maximum OTPs sent to a phone number per minute.
- Name
Per Hour- Type
- number
- Description
Default: 3. Maximum OTPs sent to a phone number per hour.
- Name
Per Day- Type
- number
- Description
Default: 10. Maximum OTPs sent to a phone number per day.
Per Device ID
- Name
Per Minute- Type
- number
- Description
Default: 1. Maximum OTPs requested from a device per minute.
- Name
Per Hour- Type
- number
- Description
Default: 3. Maximum OTPs requested from a device per hour.
- Name
Per Day- Type
- number
- Description
Default: 10. Maximum OTPs requested from a device per day.
Per Widget
- Name
Per Minute- Type
- number
- Description
Default: 10. Total OTPs sent per minute across all users.
- Name
Per Hour- Type
- number
- Description
Default: 100. Auto-calculated based on per-minute value (10x ratio).
- Name
Per Day- Type
- number
- Description
Default: 2000. Auto-calculated based on per-minute value (200x ratio).
Cooldown Duration
- Name
Duration (milliseconds)- Type
- number
- Description
Default: 300000 (5 minutes). Time period a user must wait after hitting rate limits before they can retry.
Keep in mind that there is an implicit cooldown of 1 minute each time a user can request an OTP. Regardless of the rate limiting. A user can not request OTP within 60 seconds of the last OTP request.
Security Validation
When you enter a value that exceeds security constraints, the system displays a validation warning with a recommended fix.
The validation dialog shows:
- The field that violates constraints
- Maximum allowed value for security reasons
- Current vs. suggested value comparison
- Explanation of why the limit exists
Hard limits on per-phone and per-device rates protect against abuse and ensure fair usage across all users.
Auto-Calculated Values
When you adjust the per-widget per-minute value, the per-hour and per-day values are automatically calculated using safe ratios. This ensures consistent rate limiting across all time windows.
The system displays an "Auto-calculated" indicator showing that values were derived from your per-minute input.
Circuit Breaker
The circuit breaker is your final layer of defense that automatically suspends your widget when abnormal traffic patterns are detected. It works alongside captcha and rate limiting to provide comprehensive protection.
Keep Circuit Breaker Enabled. Disabling the circuit breaker removes your last line of defense against sophisticated attacks. The circuit breaker is calibrated to only trigger during genuine flood attacks—if it activates, it means it protected you from a real threat.
Why Circuit Breaker Matters:
- Blocks Coordinated DDoS Attacks: Detects and stops distributed attacks from multiple sources attempting to overwhelm your widget
- Protects Your Quota: Prevents sophisticated attacks from draining your API quota and incurring unexpected costs
- Last Line of Defense: Catches threats that bypass captcha and rate limiting (while rare, it's possible with advanced attack techniques)
- Automatic Recovery: Temporarily suspends the widget during attacks and automatically resumes normal operation when the threat subsides
Flood Thresholds
Define how many requests trigger the circuit breaker. These thresholds detect abnormal traffic spikes across different time windows.
- Name
Per Minute- Type
- number
- Description
Default: 100. Maximum requests allowed per minute before circuit breaker triggers.
- Name
Per 5 Minutes- Type
- number
- Description
Default: 500. Maximum requests allowed per 5-minute window.
- Name
Per 15 Minutes- Type
- number
- Description
Default: 1500. Maximum requests allowed per 15-minute window.
These thresholds are designed to allow legitimate traffic bursts while catching sustained attack patterns. Most applications never hit these limits under normal usage.
Suspension Durations
How long the widget stays suspended after each violation. Durations increase with repeated violations to discourage persistent attackers.
- Name
First Violation- Type
- number
- Description
Default: 300000ms (5 minutes). Initial suspension period after first threshold breach.
- Name
Second Violation- Type
- number
- Description
Default: 900000ms (15 minutes). Suspension period for second violation.
- Name
Maximum Violation- Type
- number
- Description
Default: 1800000ms (30 minutes). Maximum suspension period for repeated violations.
Tip: 1 minute = 60000ms, 5 minutes = 300000ms, 15 minutes = 900000ms, 30 minutes = 1800000ms
After Widget Creation
Once you create the widget, you'll receive credentials needed for API integration:
- Name
Widget ID- Type
- string
- Description
Internal identifier (e.g.,
widget_a1b2c3d4...)
- Name
Public Key- Type
- string
- Description
Used in API requests (e.g.,
pk_x1y2z3...)
- Name
Secret Key- Type
- string
- Description
Used to sign API requests with HMAC-SHA256
The widget secret will only be displayed once at creation. Make sure you copy it as seen in the screenshot. If you lose it, you'll need to regenerate a new secret from the dashboard. Store your secret key in environment variables. Never commit it to version control or expose it to frontend code.
Step 2: Backend - Create Attempt
Your backend is responsible for initiating the authentication flow by creating an "attempt". This is a server-side operation that must never be done from the frontend to protect your widget secret.
Understanding the Flow
What happens when you create an attempt:
- User requests authentication - Your frontend collects the phone number and sends it to your backend
- Your backend creates a signature - Using your widget secret, you generate an HMAC-SHA256 signature to prove you own the widget
- Your backend calls Akedly API - Send the signed request to create an attempt
- Akedly returns an iframe URL - You receive a unique URL that opens the authentication widget
- Your backend sends URL to frontend - Pass the
iframeUrlto your frontend to display the widget
Security Rule: The widget secret must NEVER leave your server. All signature generation and API calls happen on your backend only.
Why HMAC Signatures?
HMAC-SHA256 signatures prove that the request came from someone who knows the widget secret, without exposing the secret itself. This prevents attackers from creating unauthorized authentication attempts even if they know your public key.
Signature Generation (Language-Agnostic)
The signature is the most critical part. Here's how to generate it in any language:
Step 1: Prepare the message
Create a JSON string with these exact keys in this exact order:
Message Format
{
"apiKey": "YOUR_API_KEY",
"publicKey": "YOUR_PUBLIC_KEY",
"timestamp": 1234567890123,
"phoneNumber": "+1234567890"
}
Step 2: Generate HMAC-SHA256
Use your widget secret as the key and the JSON string as the message:
Pseudocode
signature = HMAC-SHA256(secret, message)
output = hex_encode(signature)
Step 3: Include in request
Send the hex-encoded signature in the signature field of your API request.
Common Signature Mistakes
- Wrong JSON order: Keys must be in exact order:
apiKey,publicKey,timestamp,phoneNumber - Extra spaces: Use compact JSON with no spaces after colons or commas
- Wrong encoding: Both secret and message must be UTF-8 encoded
- Stale timestamp: Timestamp must be within 5 minutes of server time
API Reference
Required Parameters
- Name
apiKey- Type
- string
- Description
Your Akedly API key from the dashboard API section
- Name
publicKey- Type
- string
- Description
The widget's public key (from widget creation)
- Name
signature- Type
- string
- Description
HMAC-SHA256 signature of the request payload
- Name
timestamp- Type
- number
- Description
Current Unix timestamp in milliseconds. Must be within 5 minutes of server time.
- Name
verificationAddress- Type
- object
- Description
Contact information for OTP delivery. Include
phoneNumber(with country code) and/oremail.
- Name
digits- Type
- number
- Description
OTP length:
4or6. Defaults to6.
- Name
otp- Type
- string
- Description
Bring-your-own OTP (4 or 6 digits). When provided, billing switches to pay-per-message instead of pay-per-verification.
Ensure that the phone number is in E.164 format with country code (e.g.
+20155664423). Make sure it's normalized (no spaces, dashes, or
parentheses). For a list of country codes, see E.164 Country
Codes.
Response
- Name
status- Type
- string
- Description
"success" or "error"
- Name
data.attemptId- Type
- string
- Description
Unique attempt identifier (e.g.,
attempt_a1b2c3d4e5f6...)
- Name
data.iframeUrl- Type
- string
- Description
Full URL to open in iframe (e.g.,
https://auth.akedly.io/auth?attemptId=xxx)
- Name
data.expiresAt- Type
- string
- Description
ISO 8601 timestamp when attempt expires (5 minutes from creation)
Request Body
{
"apiKey": "61b7fgxxxxxxxxxxxx", //Account API key
"publicKey": "pk_xxxxxxxxxxxx", //Widget's public key
"signature": "a1b2c3d4e5f6789...", //Secret key
"timestamp": 170155235400,
"verificationAddress": {
"phoneNumber": "+201556645234", // MUST have country code
"email": "user@example.com" //optional
},
"digits": 6 //choose between 4 or 6
}
Response
{
"status": "success",
"data": {
"attemptId": "attempt_a1b2c3d4e5f6...",
"iframeUrl": "https://auth.akedly.io/auth?attemptId=...",
"expiresAt": "2025-01-16T12:05:00.000Z"
}
}
Implementation Examples
Complete code examples for creating an authentication attempt in popular backend languages.
Backend Implementation
const crypto = require('crypto')
const axios = require('axios')
// Environment variables (NEVER expose these in frontend)
const AKEDLY_API_KEY = process.env.AKEDLY_API_KEY
const WIDGET_PUBLIC_KEY = process.env.WIDGET_PUBLIC_KEY
const WIDGET_SECRET = process.env.WIDGET_SECRET
function generateSignature(apiKey, publicKey, secret, timestamp, phoneNumber) {
const message = JSON.stringify({
apiKey,
publicKey,
timestamp,
phoneNumber,
})
return crypto.createHmac('sha256', secret).update(message).digest('hex')
}
async function createAuthAttempt(phoneNumber, email = null) {
const timestamp = Date.now()
const signature = generateSignature(
AKEDLY_API_KEY,
WIDGET_PUBLIC_KEY,
WIDGET_SECRET,
timestamp,
phoneNumber,
)
const response = await axios.post(
'https://api.akedly.io/api/v1/widget-sdk/create-attempt',
{
apiKey: AKEDLY_API_KEY,
publicKey: WIDGET_PUBLIC_KEY,
signature,
timestamp,
verificationAddress: {
phoneNumber,
email,
},
digits: 6,
},
)
return response.data.data // { attemptId, iframeUrl, expiresAt }
}
// Usage in Express route
app.post('/api/auth/start', async (req, res) => {
try {
const { phoneNumber, email } = req.body
const attempt = await createAuthAttempt(phoneNumber, email)
res.json({
success: true,
attemptId: attempt.attemptId,
iframeUrl: attempt.iframeUrl,
})
} catch (error) {
res.status(500).json({
success: false,
error: error.response?.data || error.message,
})
}
})
Step 3: Frontend - Open Widget
Once you receive the attemptId and iframeUrl from your backend, open the Akedly authentication widget in an iframe or WebView.
Implementation Options
Option 1: Iframe in Modal/Dialog (Recommended for Web)
Open the widget in a modal dialog for the best user experience. The widget is designed to fit perfectly in a 500px × 700px iframe.
Option 2: WebView in Native App (Recommended for Mobile)
Use a WebView component in Flutter or React Native to display the widget with full native integration.
Option 3: Iframe in Page
Embed the widget directly in your page layout.
Handling Results
The widget communicates results through two mechanisms:
- URL Redirect (if
frontendCallbackURLis configured in pipeline) - PostMessage API (for web) or Navigation Detection (for WebViews)
Frontend Implementation
import { useState, useEffect } from 'react'
export default function AuthModal() {
const [isOpen, setIsOpen] = useState(false)
const [iframeUrl, setIframeUrl] = useState('')
const [loading, setLoading] = useState(false)
const startAuth = async (phoneNumber, email = null) => {
setLoading(true)
try {
const response = await fetch('/api/auth/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phoneNumber, email }),
})
const data = await response.json()
if (!data.success) {
throw new Error(data.error)
}
setIframeUrl(data.iframeUrl)
setIsOpen(true)
} catch (error) {
alert('Failed to start authentication: ' + error.message)
} finally {
setLoading(false)
}
}
// Listen for postMessage from iframe
useEffect(() => {
const handleMessage = (event) => {
if (event.origin !== 'https://auth.akedly.io') return
if (event.data.type === 'AUTH_SUCCESS') {
console.log('Authentication successful!', event.data)
setIsOpen(false)
onAuthSuccess(event.data)
} else if (event.data.type === 'AUTH_FAILED') {
alert('Authentication failed')
}
}
window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [])
const onAuthSuccess = (data) => {
// Your success logic
window.location.href = '/dashboard'
}
return (
<>
<button
onClick={() => startAuth('+201234567890', 'user@example.com')}
disabled={loading}
>
{loading ? 'Loading...' : 'Login with Phone'}
</button>
{isOpen && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999,
}}
onClick={(e) => {
if (e.target === e.currentTarget) setIsOpen(false)
}}
>
<iframe
src={iframeUrl}
style={{
width: '500px',
height: '700px',
border: 'none',
borderRadius: '12px',
background: 'white',
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.3)',
}}
/>
</div>
)}
</>
)
}
Step 4: Handle Callbacks
Backend Webhook Payload
If you configure a backendCallbackURL in your pipeline settings, Akedly sends a POST request to your server with complete verification details.
Webhook Timing:
The webhook is sent:
- Immediately after successful verification (OTP verified)
- Immediately after failed verification (invalid OTP, expired, etc.)
Success Webhook Structure:
- Name
status- Type
- string
- Description
"success"
- Name
timestamp- Type
- string
- Description
ISO 8601 timestamp of event
- Name
widgetAttempt- Type
- object
- Description
Complete widget attempt details
- Name
transaction- Type
- object
- Description
MainTransaction object with verification details
- Name
transactionReq- Type
- object
- Description
TransactionReq object with OTP delivery details
Webhook Payloads
{
"status": "success",
"timestamp": "2025-01-16T12:05:30.123Z",
"widgetAttempt": {
"attemptId": "attempt_a1b2c3d4e5f67890",
"widgetId": "widget_x1y2z3",
"userId": "67890abcdef12345",
"status": "verified",
"verificationAddress": {
"phoneNumber": "+20****7890",
"email": "user@example.com"
},
"otpConfig": {
"digits": 6,
"resendCount": 0
},
"createdAt": "2025-01-16T12:00:00.000Z",
"expiresAt": "2025-01-16T12:05:00.000Z",
"captchaVerifiedAt": "2025-01-16T12:01:15.500Z",
"otpRequestedAt": "2025-01-16T12:01:30.200Z",
"completedAt": "2025-01-16T12:05:30.123Z"
},
"transaction": {
"transactionID": "ae2eacaebe3ed78b105498d5d0cfe54f",
"status": "Successful",
"verificationAddress": {
"phoneNumber": "+201234567890",
"email": "user@example.com"
},
"OTP": "123456",
"creationDate": "2025-01-16T12:01:25.000Z",
"expirationDate": "2025-01-16T12:04:25.000Z",
"updateDate": "2025-01-16T12:05:30.123Z",
"userID": "67890abcdef12345",
"pipelineID": "abc123pipeline"
},
"transactionReq": {
"_id": "req_9876543210",
"status": "Successful",
"mainTransactionID": "ae2eacaebe3ed78b105498d5d0cfe54f",
"sentVerification": true,
"creationDate": "2025-01-16T12:01:30.000Z",
"expirationDate": "2025-01-16T12:04:30.000Z",
"sentVerificationDate": "2025-01-16T12:01:31.500Z",
"inputOTP": "123456",
"verificationDate": "2025-01-16T12:05:30.123Z"
}
}
Webhook Handler Example
const express = require('express')
const app = express()
app.use(express.json())
app.post('/api/webhooks/akedly', async (req, res) => {
try {
const { status, widgetAttempt, transaction, transactionReq, error } =
req.body
console.log('Received webhook:', {
status,
attemptId: widgetAttempt.attemptId,
transactionId: transaction.transactionID,
})
if (status === 'success') {
// Mark user as verified in your database
await db.users.update(
{ phoneNumber: transaction.verificationAddress.phoneNumber },
{
verified: true,
verifiedAt: new Date(),
akeledyTransactionId: transaction.transactionID,
},
)
// Send welcome email, create session, etc.
await sendWelcomeEmail(transaction.verificationAddress.email)
console.log('User verified successfully')
} else {
// Log failed attempt
await db.authAttempts.create({
attemptId: widgetAttempt.attemptId,
phoneNumber: transaction.verificationAddress.phoneNumber,
status: 'failed',
errorCode: error?.code,
errorMessage: error?.message,
timestamp: new Date(),
})
console.log('Authentication failed:', error?.code)
}
// Always respond with 200 to acknowledge receipt
res.status(200).json({ received: true })
} catch (error) {
console.error('Webhook processing error:', error)
// Still respond with 200 to prevent retries
res.status(200).json({ received: true, error: error.message })
}
})
app.listen(3000, () => {
console.log('Webhook server listening on port 3000')
})
Security Features
V2.0 Widgets include enterprise-grade security features at no additional cost. Our multi-layered approach combines visible and invisible protection mechanisms.
Proprietary Security Layers
In addition to the security features documented below, V2.0 Widgets include 3 additional proprietary security layers that operate behind the scenes. These undisclosed mechanisms handle a significant portion of fraud prevention and are kept confidential for security reasons.
Bot Protection
Invisible bot protection runs automatically before any OTP delivery, blocking automated attacks without user friction.
What It Does:
- Validates request authenticity using behavioral analysis
- Blocks bot traffic before OTP delivery
- Reduces fraudulent authentication attempts
- Saves costs by preventing fake verification requests
Device Intelligence
Advanced device analysis creates risk profiles to enable fraud detection and enforce security policies.
Capabilities:
- Generates unique device identifiers for tracking
- Detects suspicious behavior patterns across sessions
- Enables device-based rate limiting and analytics
- Supports fraud metrics in your dashboard
Specific fingerprinting attributes are intentionally not disclosed to prevent reverse-engineering attempts.
Circuit Breaker
Automatic flood protection suspends widgets under attack with progressive suspension durations.
How It Works:
- Monitors traffic patterns across multiple time windows
- Triggers automatically when abnormal activity is detected
- Applies progressive suspension durations
- Resumes automatically when threat subsides
Rate Limiting
Multi-dimensional rate limiting protects at phone number, device, and widget levels.
Configurable Limits:
- Per phone number limits (attempts and OTP requests)
- Per device limits (attempts and OTP requests)
- Per widget global limits
Default values are suitable for most use cases. Configure custom limits in Dashboard → Widgets → Security Settings.
Error Reference
Error Format
All errors follow this format:
{
"status": "error",
"code": "ERROR_CODE",
"message": "Human-readable description",
"retryable": true, // Optional: whether user can retry
"retryAfter": "ISO8601", // Optional: when to retry (for rate limits)
"cooldownSeconds": 60, // Optional: seconds until retry allowed
"details": {} // Optional: additional context
}
HTTP Status Codes
| Status Code | Description |
|---|---|
| 400 | Invalid request parameters, missing fields |
| 401 | Invalid credentials, expired signature |
| 403 | Permission denied, inactive widget, verification not complete |
| 404 | Resource not found (widget, attempt, transaction) |
| 409 | Resource already used (captcha token reused) |
| 410 | Resource expired (attempt or transaction expired) |
| 429 | Rate limit exceeded |
| 500 | Server-side error during processing |
| 502 | External service error (Cloudflare Turnstile API) |
| 503 | Circuit breaker triggered (flood protection) |
Authentication & Security Errors
- Name
INVALID_API_KEY- Description
The provided API key is invalid or doesn't exist.
Solution: Verify your API key in the dashboard at Settings → API.
- Name
INVALID_PUBLIC_KEY- Description
The widget public key doesn't exist.
Solution: Check your widget public key in the dashboard under Widgets.
- Name
INVALID_SIGNATURE- Description
HMAC signature validation failed.
Solution: Ensure you're generating the signature correctly using the widget secret. Verify the message payload matches exactly (JSON stringify with no extra spaces).
- Name
SIGNATURE_EXPIRED- Description
The timestamp in the signature is too old (>5 minutes) or in the future.
Solution: Ensure your server clock is synchronized (use NTP). Generate timestamp immediately before creating signature.
- Name
WIDGET_USER_MISMATCH- Description
The widget doesn't belong to the user associated with the API key.
Solution: Ensure you're using the correct API key and public key pair.
- Name
WIDGET_INACTIVE- Description
The widget status is set to "inactive" or "suspended".
Solution: Activate the widget in the dashboard under Widgets → [Your Widget] → Status.
- Name
WIDGET_NOT_FOUND- Description
Widget with the provided public key doesn't exist.
Solution: Verify your public key is correct.
- Name
ATTEMPT_NOT_FOUND- Description
Authentication attempt not found or doesn't match device.
Solution: Ensure you're using the correct attemptId. User may need to restart authentication.
- Name
ATTEMPT_EXPIRED- Description
Authentication attempt has expired (>5 minutes old).
Solution: User must start a new authentication attempt. Show "Session expired" message.
- Name
TRANSACTION_EXPIRED- Description
OTP transaction has expired (>3 minutes since OTP was sent).
Solution: User can request a new OTP (if resend limit not exceeded) or start over.
Rate Limiting Errors
All rate limit errors include retryAfter (ISO8601 timestamp) and cooldownSeconds (integer).
- Name
RATE_LIMIT_PHONENUMBER_ATTEMPTS- Description
Too many authentication attempts for this phone number.
Solution: User must wait until
cooldownSecondsexpires. Show countdown timer.
- Name
RATE_LIMIT_PHONENUMBER_OTP- Description
Too many OTP requests for this phone number.
Solution: User must wait before requesting another OTP.
- Name
RATE_LIMIT_DEVICEID_ATTEMPTS- Description
Too many authentication attempts from this device.
Solution: Device-based rate limit. User must wait or try from different device.
- Name
RATE_LIMIT_WIDGET_ATTEMPTS- Description
Too many authentication attempts for this widget (global).
Solution: Your widget is receiving high traffic. Contact support to increase limits.
Rate limits are configurable per widget in the dashboard. Default values are calibrated for typical use cases and provide strong protection against abuse.
Circuit Breaker Errors
- Name
CIRCUIT_BREAKER_OPEN- Description
Widget is currently suspended due to previous flood detection.
Solution: Wait until suspension expires. Check
suspendedUntiltimestamp in the error response.
- Name
CIRCUIT_BREAKER_TRIGGERED- Description
Widget triggered circuit breaker due to flood threshold exceeded.
Solution: Your widget is experiencing abnormal traffic. Monitor your traffic patterns and contact support if this persists.
Circuit breaker thresholds and suspension durations are configurable in Dashboard → Widgets → Security Settings. The system applies progressive suspension periods for repeated violations.
Advanced Features
Bring Your Own OTP
If you want to generate your own OTP codes, pass a custom otp parameter:
const response = await axios.post(
'https://api.akedly.io/api/v1/widget-sdk/create-attempt',
{
apiKey: AKEDLY_API_KEY,
publicKey: WIDGET_PUBLIC_KEY,
signature,
timestamp,
verificationAddress: { phoneNumber: '+201234567890' },
otp: '123456', // Your custom 6-digit OTP
},
)
Billing Difference
- Standard (Akedly-generated OTP): Pay-per-successful-verification - Custom OTP: Pay-per-message (billed immediately when OTP is sent)
Troubleshooting
INVALID_SIGNATURE Error
Common causes:
- Incorrect HMAC algorithm (must be SHA256)
- Wrong JSON key order in signature message
- Extra spaces in JSON string
- Widget secret is incorrect
- Timestamp is stale (>5 minutes old)
Solutions:
- Verify you're using HMAC-SHA256
- Ensure message format is
JSON.stringify({ apiKey, publicKey, timestamp, phoneNumber }) - Check your widget secret matches the dashboard
- Synchronize server clock with NTP
Iframe Shows Blank Screen
Common causes:
- Invalid or malformed attemptId in URL
- Attempt has expired (5-minute lifetime)
- Parent page not served over HTTPS
- Browser blocking mixed content
Solutions:
- Check browser console for errors
- Verify iframeUrl contains a valid attemptId
- Ensure parent page uses HTTPS in production
- Confirm attempt hasn't expired
PIPELINE_NOT_CONFIGURED Error
Solution:
- Go to Dashboard → Widgets → [Your Widget]
- Edit widget settings
- Select a pipeline from the dropdown
- Save changes
OTP Not Received
Common causes:
- Phone number format is incorrect
- Rate limit exceeded for phone number
- Pipeline verification methods not configured
Solutions:
- Ensure phone number uses E.164 format with country code (e.g.,
+201234567890) - Check rate limit status in dashboard analytics
- Verify pipeline has at least one verification method enabled
Support & Resources
Getting Help
- Dashboard: https://app.akedly.io
- Support: support@akedly.io
- Co-founders: muhad@akedly.io, hana@akedly.io
Additional Resources
Version: 2.0.0
Last Updated: November 25th, 2025
API Base URL: https://api.akedly.io/api/v1/widget-sdk