Skip to content
Akedly
V2.0 Widgets

Create an attempt

Sign a request on your backend and create an attempt, with code examples for popular backend languages.

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

For a phone-bearing request, create a JSON string with these exact keys in this exact order: apiKey, publicKey, timestamp, phoneNumber. Email-only signing is shown below; the email address is not part of the HMAC message.

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 Account & Billing → API Keys in the dashboard

  • 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 60 seconds of server time in either direction (absolute delta), so generate it immediately before signing.

  • Name
    verificationAddress
    Type
    object
    Description

    Contact information for OTP delivery. Include phoneNumber (with country code) and/or email.

  • Name
    digits
    Type
    number
    Requirement
    Optional
    Description

    OTP length: 4, 5 or 6. Defaults to 6. Unlike V1.2, V2 rejects an out-of-range value with INVALID_OTP_DIGITS. The hosted widget renders whatever length the attempt carries — to try one without sending anything, see Dev Mode & Test Pairs.

  • Name
    otp
    Type
    string
    Requirement
    Optional
    Description

    Bring-your-own OTP (4, 5 or 6 digits). When provided, billing switches to pay-per-message instead of pay-per-verification. If you send both otp and digits, the OTP's length must match digits or the request fails with OTP_DIGITS_MISMATCH.

  • Name
    otpLanguage
    Type
    string
    Requirement
    Optional
    Description

    OTP message language for WhatsApp and SMS. Overrides the pipeline's OTP Language for this attempt. Set it on your server at create-attempt. It is unsigned and is fixed for that attempt, even if the pipeline language changes later; browser request-otp and resend-otp calls cannot change it. It is separate from any language value you put in publicMetadata, data.template.language and the widget’s screen language. Invalid values return HTTP 400 with code: "INVALID_OTP_LANGUAGE" and message. See accepted values and defaults.

  • Name
    template
    Type
    object
    Requirement
    Optional
    Description

    { id, variables } — send this attempt's WhatsApp OTP in your own approved OTP template wording. Set it on your server only: the signature does not cover template, and the widget cannot change it later. It must be Approved in the language the OTP uses (otpLanguage when supplied, otherwise the pipeline's OTP Language), or the request fails with a 400 before anything is sent.

  • 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, content-length and anything beginning svix- are blacklisted, silently filtered out before dispatch, and the filtered result is persisted with the attempt. The svix- prefix is reserved because that is where Akedly puts the callback's own signature headers — supplying your own would let a caller overwrite them.

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)

  • Name
    data.otpLanguage
    Type
    string
    Description

    Always present: en or ar. The language the OTP is requested in, not a delivery receipt. Browser-facing V2 responses are unchanged.

  • Name
    data.template
    Type
    object
    Description

    Present when a custom template was selected, in the request or by the pipeline default: { id, language } as resolved at creation. request-otp and resend-otp return the same value. It does not confirm which channel or template delivered the code.

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=1&meta_orderId=2

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


Implementation Examples

Complete code examples for creating an authentication attempt in popular backend languages.

Backend Implementation

POSTapi.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 }
}

// 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) {
    const upstream = error.response?.data
    res.status(error.response?.status || 500).json({
      success: false,
      message: 'Could not start verification. Please try again.',
      code: upstream?.code,
      retryable: upstream?.retryable,
      retryAfter: upstream?.retryAfter,
      cooldownSeconds: upstream?.cooldownSeconds,
    })
  }
})