Skip to content
Akedly
V1.2 REST API

Get a challenge and send the OTP

Request a challenge, send the OTP with the proofs it requires, and set the OTP message language.

Step 1: Get Challenge

Request the challenge and Turnstile configuration for the given pipeline. The response tells your client which work is required before sending an OTP.

Query Parameters

  • Name
    APIKey
    Type
    string
    Description

    Your API key, from Account & Billing → API Keys in the dashboard.

  • Name
    pipelineID
    Type
    string
    Description

    The unique identifier of your pipeline, found in Basic Details.

Response Fields

  • Name
    challengeRequired
    Type
    boolean
    Description

    Whether PoW is required for this pipeline. Branch on this field first. When it is false the response contains no challenge, difficulty, challengeToken, expiresAt, algorithm or instructions at all — skip PoW entirely and omit powSolution from Step 2.

  • Name
    challenge
    Type
    string (when challengeRequired is true)
    Description

    64-character hex string. The PoW challenge to solve. Present only when challengeRequired is true and omitted when PoW is disabled for the pipeline.

  • Name
    difficulty
    Type
    number (when challengeRequired is true)
    Description

    Number of leading hex zeros required in the hash. Higher = harder. Set by the server per request and adaptive to traffic — see Difficulty is adaptive. Never hardcode it.

  • Name
    challengeToken
    Type
    string (when challengeRequired is true)
    Description

    Server-signed token binding this challenge to your request. Pass it back in Step 2. Omitted when PoW is disabled for the pipeline.

  • Name
    expiresAt
    Type
    string
    Description

    ISO 8601 timestamp for when this challenge stops being accepted. The TTL is not constant — it scales with difficulty, so read this field rather than assuming a fixed window. Budget your solve-and-retry against it; once it passes, request a fresh challenge.

  • Name
    algorithm
    Type
    string
    Description

    Hash algorithm for the challenge. Currently always "sha256".

  • Name
    instructions
    Type
    string
    Description

    Human-readable description of the work required, for logging and debugging. Do not parse it.

  • Name
    devMode
    Type
    boolean
    Description

    Present and true only while the pipeline's dev mode is active and bypasses PoW. Together with challengeRequired: false, the response then carries the message "Dev mode active - PoW challenge bypassed".

  • Name
    turnstile.required
    Type
    boolean
    Description

    Whether Turnstile verification is required for this pipeline. Off by default.

  • Name
    turnstile.siteKey
    Type
    string | null
    Description

    Cloudflare Turnstile site key when configured; otherwise null. Use it with the Shield SDK only when turnstile.required is true.


Step 2: Send OTP

Submit the applicable proofs along with the user's phone number or email address. The server validates enabled proofs, then creates and sends the OTP in a single call.

Request Body

  • Name
    APIKey
    Type
    string
    Description

    Your API key, from Account & Billing → API Keys in the dashboard.

  • Name
    pipelineID
    Type
    string
    Description

    Your pipeline ID.

  • Name
    verificationAddress
    Type
    JSON
    Description

    JSON object with phoneNumber, email, or both. At least one address is required; each supplied address must have a matching verification method in the pipeline. Arabic-Indic (٠-٩) and Persian (۰-۹) digits in phoneNumber are accepted and converted to ASCII digits.

  • Name
    powSolution
    Type
    JSON
    Description

    Object containing challengeToken (from Step 1) and nonce (from the PoW solver). Include it only when challengeRequired is true.

  • Name
    turnstileToken
    Type
    string
    Description

    Turnstile verification token. Required when turnstile.required was true in Step 1.

  • Name
    otp
    Type
    string
    Requirement
    Optional
    Description

    Bring your own 4, 5 or 6 digit OTP code. When provided, billing switches to pay-per-message upon send.

  • Name
    digits
    Type
    number
    Requirement
    Optional
    Description

    OTP length, 4, 5 or 6. Defaults to 6. Out-of-range values are ignored and fall back to 6 — V1.2 never rejects a request over digits. To try a non-default length without sending anything, see Dev Mode & Test Pairs.

  • Name
    channel
    Type
    string
    Requirement
    Optional
    Description

    Send this code on a specific channel — whatsapp, telegram or sms. Requires verificationAddress.phoneNumber. Omit it and the pipeline routes as it always has. Must be switched on for your account and pipeline first — see Letting Users Choose Their Channel.

  • Name
    otpLanguage
    Type
    string
    Requirement
    Optional
    Description

    OTP message language for WhatsApp and SMS. Overrides the pipeline's OTP Language for this request. See accepted values and defaults.

  • Name
    template
    Type
    object
    Requirement
    Optional
    Description

    { id, variables } — send this OTP's WhatsApp message in your own approved OTP template wording. Choose it on your server. 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.

Request Headers

  • Name
    Content-Type
    Type
    string
    Description

    Must be application/json.

  • Name
    x-end-user-ip
    Type
    string
    Requirement
    Optional
    Description

    Enables the per-end-user-IP rate-limit dimension (defaults: 5/min, 20/hour, 50/day per pipeline). If you send it, it must be the real end user's IPv4 or IPv6 address — the one that hit your backend from the browser or mobile device, not your backend server's IP. When omitted, Akedly falls back to the TCP connection IP; for a backend-to-backend call that's your server, so Akedly detects loopback and private IPs (127.0.0.1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, ::1, CGNAT, link-local) and simply skips the per-IP check for them. Per-phone-number and per-pipeline limits always apply regardless.

Extracting the end-user IP on your backend

Backend
// Put this once, before any routes. Tells Express to trust X-Forwarded-For
// from your reverse proxy (Cloudflare, AWS ALB, Nginx, Fly.io, etc.).
app.set('trust proxy', 1);

app.post('/auth/akedly/send', async (req, res) => {
  // Now req.ip is the REAL end-user IP, not your LB's.
  const r = await fetch('https://api.akedly.io/api/v1.2/transactions/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-end-user-ip': req.ip,
    },
    body: JSON.stringify({ /* ... */ }),
  });
  res.status(r.status).json(await r.json());
});

All three sets of caps are configured on the pipeline itself, per minute, per hour and per day. The per-pipeline caps do double duty: they are also the denominator the adaptive PoW ramp measures utilisation against, so raising them raises the traffic level at which difficulty starts climbing.

Response Fields

  • Name
    status
    Type
    string
    Description

    "success" or "error".

  • Name
    data.transactionID
    Type
    string
    Description

    The main transaction ID for tracking.

  • Name
    data.transactionReqID
    Type
    string | null
    Description

    The transaction request ID. Save this for Step 3 verification. It can be null if the activation result carried no request record. Step 3 cannot run without it, so check it before you move on.

  • Name
    data.channels
    Type
    string[]
    Description

    A bounded activation snapshot of the unique channels persisted at response time (for example, ["whatsapp", "email"]). WhatsApp can still be awaiting a provider result when the five-second wait ends, so this is not a final delivery receipt. A later signed otp.channels.updated event can correct the set after a late WhatsApp failure. Possible values: "whatsapp", "telegram", "sms", "email".

  • Name
    data.expiresAt
    Type
    string
    Description

    ISO 8601 timestamp indicating when the transaction expires.

  • Name
    data.otpLanguage
    Type
    string
    Description

    Always present: en or ar. The language the OTP is requested in, not a delivery receipt.

  • 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. It does not confirm which channel or template delivered the code.

  • Name
    message
    Type
    string
    Description

    "OTP sent successfully".


OTP message language

Send top-level otpLanguage on V1 transaction creation, V1.2 send, or V2 create-attempt to override the pipeline's OTP Language for one request. An absent field or null uses the pipeline's OTP Language.

Accepted values are en or ar in any letter case, optionally followed by - or _ and two letters or three digits. For example, AR, ar-EG, en_GB and ar-001 resolve to ar, ar, en and ar. The raw string is checked without trimming.

Leading or trailing spaces, words such as arabic, other languages such as fr, script tags such as ar-Arab, "", numbers, arrays, objects and booleans return HTTP 400 with INVALID_OTP_LANGUAGE. Correct the value before retrying.

V1.2 checks this after the proof-of-work and Turnstile checks. When proof-of-work is enabled, a solved challenge is spent, so fetch and solve a new challenge before retrying. When Turnstile is required, obtain a fresh Turnstile token before retrying. Unlike out-of-range digits, an invalid otpLanguage is rejected.

Successful responses include data.otpLanguage — ar for the OTP language request example above — the language the OTP is requested in, not a delivery receipt.

A custom template, whether explicitly selected or the pipeline default, must be Approved in the requested language. Otherwise the request returns HTTP 400 — TEMPLATE_NOT_APPROVED, or TEMPLATE_REJECTED if that language was rejected — and never falls back to another language or the Built-in default at creation. See template errors.