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

For a detailed comparison of V2.0 vs V1.0 vs upcoming V1.2, see the Authentication Methods overview.


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&meta_userId=user_12345&meta_orderId=order_abc789

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&meta_userId=user_12345&meta_orderId=order_abc789

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)

  • Name
    meta_*
    Type
    string
    Description

    Custom metadata fields from publicMetadata. Each key is prefixed with meta_. Object values are JSON stringified.

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.

OTP Requests

OTP requests track actual One-Time Password deliveries. These limits apply only when an OTP is successfully sent to the user.

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.

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

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.

  • Name
    publicMetadata
    Type
    object
    Description

    Optional custom data returned in frontend redirect URL as meta_* query parameters. Useful for tracking IDs, session references, or order context. Max combined size with privateMetadata: 10KB.

  • Name
    privateMetadata
    Type
    object
    Description

    Optional server-only custom data included only in backend webhooks. Never sent to browser. Useful for sensitive data, auth tokens, or internal state. Max combined size with publicMetadata: 10KB.

  • Name
    customHeaders
    Type
    object
    Description

    Optional key-value pairs of custom HTTP headers forwarded in webhook callbacks to your backend. Max 4KB size limit. Headers content-type, user-agent, host, and content-length are blacklisted and will be rejected.

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
  "publicMetadata": {
    //optional
    "userId": "user_12345",
    "orderId": "order_abc789"
  },
  "privateMetadata": {
    //optional - server-only
    "internalUserId": "internal_xyz"
  },
  "customHeaders": {
    //optional - forwarded in webhooks
    "X-Correlation-ID": "corr_abc123",
    "X-Internal-Source": "checkout-flow"
  }
}

Response

{
  "status": "success",
  "data": {
    "attemptId": "attempt_a1b2c3d4e5f6...",
    "iframeUrl": "https://auth.akedly.io/auth?attemptId=...",
    "expiresAt": "2025-01-16T12:05:00.000Z",
    "publicMetadata": {
      "userId": "user_12345",
      "orderId": "order_abc789"
    }
  }
}

Custom Metadata

Attach custom data to verification attempts that gets returned in callbacks. This is useful for tracking users, orders, or sessions through the authentication flow.

Two Types of Metadata:

  • Name
    publicMetadata
    Type
    object
    Description

    Data returned in the frontend redirect URL as query parameters prefixed with meta_. Also included in backend webhooks.

    Use for: User IDs, order IDs, session references, analytics tracking, non-sensitive context.

    Example redirect: ?status=success&meta_userId=12345&meta_orderId=order_abc

  • Name
    privateMetadata
    Type
    object
    Description

    Data returned only in backend webhooks. Never sent to the browser or included in redirect URLs.

    Use for: Internal user IDs, session tokens, sensitive business data, server-side state.

Request with Metadata

{
  .........
  "publicMetadata": { //example of data sent to frontend
    "userId": "user_12345",
    "orderId": "order_abc789",
    "registerSource": "landing_page",
    "preferences": {
      "language": "en",
      "timezone": "UTC+2"
    }
  },
  "privateMetadata": { //example of server-only data
    "internalId": "internal_xyz",
    "sessionToken": "sess_abc123",
    "experimentGroup": "A",
    "sessionData": {
      "cartItems": 3,
      "lastLogin": "2025-01-15T10:00:00Z"
    }
  }
}

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, metadata = {}) {
  const { publicMetadata, privateMetadata } = metadata
  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,
      publicMetadata,
      privateMetadata,
    },
  )

  return response.data.data // { attemptId, iframeUrl, expiresAt, publicMetadata }
}

// Usage in Express route
app.post('/api/auth/start', async (req, res) => {
  try {
    const { phoneNumber, email, userId, orderId } = req.body

    const attempt = await createAuthAttempt(phoneNumber, email, {
      publicMetadata: { userId, orderId },
      privateMetadata: { internalId: req.session.internalId },
    })

    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

  • Name
    publicMetadata
    Type
    object
    Description

    Custom public metadata attached during attempt creation (if provided)

  • Name
    privateMetadata
    Type
    object
    Description

    Custom private metadata attached during attempt creation (if provided). Only available in webhooks.

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"
  },
  "publicMetadata": {
    "userId": "user_12345",
    "orderId": "order_abc789"
  },
  "privateMetadata": {
    "internalUserId": "internal_xyz",
    "sessionToken": "sess_secret_token"
  }
}

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
    METADATA_SIZE_EXCEEDED
    Description

    Combined publicMetadata and privateMetadata exceeds 10KB limit.

    Solution: Reduce the size of your metadata objects. Consider storing large data server-side and only passing references.

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

  • Name
    RESEND_LIMIT_EXCEEDED
    Description

    Too many OTP resend requests for this attempt.

    Solution: Each attempt allows a limited number of resends. Start a new authentication attempt if the user needs another OTP.

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.

External Service Errors

  • Name
    CAPTCHA_API_ERROR
    Description

    Cloudflare Turnstile API returned an error during captcha verification.

    Solution: This is a temporary external service issue. Retry the request. If persistent, check Cloudflare Status.


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: January 11th, 2026 API Base URL: https://api.akedly.io/api/v1/widget-sdk

Was this page helpful?