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.


Why Choose V2.0 Widgets

FeatureV2.0 WidgetsV1.0 Standard API
Integration Time15 minutes20+ minutes
Frontend Code5 lines100+ lines
Backend Code1 API call3 API calls
UI/UXFully managed by AkedlyYou build everything
Pricing ModelPPSA eligible (requirements apply)PPM (Pay per message) always
Captcha ProtectionBuilt-in & FREENot included
Device FingerprintingBuilt-inLimited
Circuit BreakerBuilt-inNot included
Custom BrandingLogo, colors, company nameNo customization

How V2.0 Widgets Work

The authentication flow is simple and secure:

1

Your User Initiates Authentication

User clicks "Login" or "Verify Phone" in your application.

2

Your Backend Creates Attempt

Your backend calls Akedly API with HMAC signature:

POST /api/v1/widget-sdk/create-attempt
3

Akedly 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

4

Your Frontend Opens Widget

Display the authentication widget in an iframe OR open a Webview/new tab for the user:

<iframe src={iframeUrl} />
5

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
6

Receive Verification Result 🎉

Akedly redirects user to your callback URL & sends webhook to your backend with verification status.


Step 1: Create Widget in Dashboard

Before integrating, create a widget in your Akedly dashboard.

Dashboard Navigation

  1. Log in to your Akedly dashboard at https://app.akedly.io
  2. Navigate to Widgets in the left sidebar
  3. 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.

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.

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.

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.

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.

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


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:

  1. User requests authentication - Your frontend collects the phone number and sends it to your backend
  2. Your backend creates a signature - Using your widget secret, you generate an HMAC-SHA256 signature to prove you own the widget
  3. Your backend calls Akedly API - Send the signed request to create an attempt
  4. Akedly returns an iframe URL - You receive a unique URL that opens the authentication widget
  5. Your backend sends URL to frontend - Pass the iframeUrl to your frontend to display the widget

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.


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/or email.

  • Name
    digits
    Type
    number
    Description

    OTP length: 4 or 6. Defaults to 6.

  • 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.

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

POST
api.akedly.io/api/v1/widget-sdk/create-attempt
{
  "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

POST
api.akedly.io/api/v1/widget-sdk/create-attempt
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:

  1. URL Redirect (if frontendCallbackURL is configured in pipeline)
  2. 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.

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

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

HTTP Status Codes

Status CodeDescription
400Invalid request parameters, missing fields
401Invalid credentials, expired signature
403

Permission denied, inactive widget, verification not complete

404Resource not found (widget, attempt, transaction)
409Resource already used (captcha token reused)
410Resource expired (attempt or transaction expired)
429Rate limit exceeded
500Server-side error during processing
502External service error (Cloudflare Turnstile API)
503Circuit 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 cooldownSeconds expires. 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.

Circuit Breaker Errors

  • Name
    CIRCUIT_BREAKER_OPEN
    Description

    Widget is currently suspended due to previous flood detection.

    Solution: Wait until suspension expires. Check suspendedUntil timestamp 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.


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
  },
)

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

Additional Resources


Version: 2.0.0 Last Updated: November 25th, 2025 API Base URL: https://api.akedly.io/api/v1/widget-sdk

Was this page helpful?