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
falsethe response contains nochallenge,difficulty,challengeToken,expiresAt,algorithmorinstructionsat all — skip PoW entirely and omitpowSolutionfrom Step 2.
- Name
- challenge
- Type
- string (when challengeRequired is true)
- Description
64-character hex string. The PoW challenge to solve. Present only when
challengeRequiredistrueand 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
trueonly while the pipeline's dev mode is active and bypasses PoW. Together withchallengeRequired: 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 whenturnstile.requiredistrue.
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 inphoneNumberare accepted and converted to ASCII digits.
- Name
- powSolution
- Type
- JSON
- Description
Object containing
challengeToken(from Step 1) andnonce(from the PoW solver). Include it only whenchallengeRequiredistrue.
- Name
- turnstileToken
- Type
- string
- Description
Turnstile verification token. Required when
turnstile.requiredwastruein 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,5or6. Defaults to6. Out-of-range values are ignored and fall back to6— V1.2 never rejects a request overdigits. 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,telegramorsms. RequiresverificationAddress.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 (otpLanguagewhen 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.
If you opt in, forward the user's IP — not your server's
Per-end-user-IP rate limiting is off by default for backend-proxy setups and only activates when you send x-end-user-ip. The usual mistake is sending your load balancer's IP instead of the user's, which quietly caps every user against a single identifier. Below are safe extraction patterns per framework — skip this section entirely if you don't need per-IP rate limiting (phone and pipeline limits still apply).
// 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());
});
Three independent rate-limit dimensions
V1.2 supports three rate-limit dimensions: per phone number and per pipeline (always on), and per end-user IP (opt-in via x-end-user-ip). If you do opt in from a mobile or native app that proxies through your backend, pass the IP of the device that called you — do not read the device's IP on the device itself; clients cannot reliably know their own public IP and can lie.
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
nullif 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 signedotp.channels.updatedevent 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:
enorar. 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".
Custom WhatsApp wording
Add a template in the dashboard, wait for Approved, then pass it as template: { id }.
See the OTP templates guide for variables, language, pipeline defaults and fallback.
Multi-Channel Delivery
OTP delivery follows smart channel routing: WhatsApp (preferred), Telegram (if available), SMS (fallback), and email (always sent when address provided). The channels array in the response is a bounded activation snapshot, not a final delivery receipt. For a later WhatsApp failure or fallback update, consume the signed otp.channels.updated event.
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.
WhatsApp and SMS language
The language applies to WhatsApp and SMS. On your own WhatsApp account, a Built-in default Arabic OTP may arrive in English until the Arabic template is approved. The template is set up automatically. Approval usually takes seconds, but this timing is not guaranteed. Akedly's own WhatsApp sender has its templates pre-approved. Arabic SMS is kept to one message, so a long company name can be shortened. A custom template that is not approved in the requested language returns HTTP 400 and never falls back.
