V1.0 Sunset Extended to December 30, 2026
The V1.0 REST API sunset has been extended to December 30, 2026 based on community feedback. Existing integrations will continue to work until then. New integrations should use the V2.0 Widget (recommended) or the V1.2 REST API.
V1.2 REST API — Replacement
V1.2 is the REST API for teams that want full control of the UI. Same V1 simplicity, with configurable Proof-of-Work and Cloudflare Turnstile when enabled for the pipeline, plus pipeline-level rate limiting and circuit breaking — no iframe required. Smooth upgrade path from V1.0. View V1.2 docs
Step 1: Create an Authentication Transaction
After setting up your pipeline, initiate an authentication transaction when a user needs to be authenticated. This generates a unique transaction ID that will be used throughout the authentication process.
Looking for a simpler integration? Check out our official SDKs for Node.js and Flutter that handle this entire flow with just a few method calls.
Request Parameters
If you include the email field in verificationAddress, ensure you've enabled email as a verification method in your pipeline settings at https://app.akedly.io/pipeline/${pipelineID}
- 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 the pipeline's basic details
- Name
- verificationAddress
- Type
- JSON
- Description
JSON object containing the user's contact information for OTP delivery. Include
phoneNumberwith country code and/oremail. For country codes, reference https://countrycode.org/
- Name
- otp
- Type
- string
- Requirement
- Optional
- Description
Bring your own 4, 5 or 6 digit OTP code (numbers only). When provided, billing switches to pay-per-message upon activation instead of pay-per-successful-authentication. Step 3 stays required: send every code your user enters to the verify call, even when your own server also checks it.
- 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
- digits
- Type
- number
- Requirement
- Optional
- Description
Specifies the length of the OTP to be generated. Accepts
4,5or6. Defaults to6if not provided.
- Name
- template
- Type
- object
- Requirement
- Optional
- Description
{ id, variables }for your own approved OTP template. Checked here;activatesends with it. An invalid selection fails with a 400 before anything is sent.
- Name
- waitForChannels
- Type
- boolean
- Requirement
- Optional
- Description
When set to
true, the API waits up to 5 seconds for channel outcomes before responding, then returns a bounded activation snapshot. A channel can still be pending when the wait ends, and a later signedotp.channels.updatedevent can report a late WhatsApp failure or fallback. When omitted orfalse, the response reports the channels attempted at dispatch time and does not wait for confirmations. This typically adds about 1 second to response time; 5 seconds is the maximum wait.
Response Format
- Name
- status
- Type
- string
- Description
The status of your transaction request ("success" or "error")
- Name
- data
- Type
- JSON
- Description
Contains the critical
transactionID- your authorization token to proceed with OTP delivery. Save this transactionID as you'll need it for the next step.
- 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:
{ id, language }as resolved at creation. It does not confirm which channel or template delivered the code.
- Name
- message
- Type
- string
- Description
Human-readable description of the transaction status
Best Practice: Store the transactionID in your database alongside the user's current session or operation. This allows you to track which user corresponds to which authentication attempt when they return to your application after verification.
For example, if you're implementing user registration with email verification, save the transactionID with the new user's record to mark them as verified once authentication completes.
Step 2: Activate Transaction and Send OTP
With your transactionID from Step 1, activate the transaction to trigger OTP delivery to the user's specified contact information.
{
//empty body
}
The first re-activation after an accepted first send sends the same code once more, returning message: "OTP sent successfully", dispatched: true, no alreadyDispatched, and a new request _id in data. Re-activation follows Step 2's rules: the transaction must still be Pending and less than 2 minutes old, counted from Step 1; otherwise it returns 404 or 403, so offer any resend inside that window. After the one resend, a re-activation returns the newest request in data with top-level alreadyDispatched: true and dispatched: false: that call sent nothing new, and it does not tell you whether the resend reached the user. While the resend is still being sent, a re-activation usually gets 409 OTP_DISPATCH_IN_PROGRESS; for a brief moment as the resend starts it can instead return the earlier request. If the resend fails before reaching any provider, a later re-activation may send it; an unconfirmed outcome and some internal errors keep the one resend used. Both requests carry the same code, but each request's 3-minute validity starts when that request is created, so verify with the newest data._id. To get a fresh code, create a new transaction.
Activation dispatch responses
| Status | Error | Cause & Solution |
|---|---|---|
| 409 | OTP_DISPATCH_IN_PROGRESS | Another call holds this send: it is still in flight, or it finished but its result is not readable yet. retryable is true; read retryAfterSeconds (3) from the body; the response also sets Retry-After: 3, which browsers may not expose cross-origin. Retry the same call after that many seconds. Expect 409 while in flight or while the result is not readable yet. Once the first send is accepted, a retry may use the one resend described in the note above, and only within the 2-minute activation window. To get a fresh code, create a new transaction. |
| 409 | OTP_DISPATCH_OUTCOME_UNKNOWN | The first send's outcome could not be confirmed. retryable is false; do not retry the same transaction. Create a new transaction in Step 1, then activate it. |
{
"status": "error",
"message": "Your verification code is still being sent. Please wait a few seconds and try again.",
"code": "OTP_DISPATCH_IN_PROGRESS",
"reason": "dispatch_in_progress",
"retryable": true,
"retryAfterSeconds": 3
}
How OTP Delivery Works
Upon successful activation, Akedly delivers the OTP using our smart multi-channel approach:
Multi-Channel Delivery:
- Email: Always sent when email address is provided
- Phone: Delivered through the most reliable available channel:
- WhatsApp (if available on the number)
- Telegram (if available and WhatsApp is not)
- Local SMS (for Egyptian numbers without WhatsApp/Telegram)
- International SMS (for international numbers without WhatsApp/Telegram)
Key Benefits:
- Guaranteed delivery through multiple channels increases success rates
- Flexible billing options based on your OTP preference
Billing Model Differences:
Standard Authentication (Akedly-generated OTP):
- Pay-per-successful-authentication - Only charged when user completes Step 3 verification
- No charge if user fails to verify or abandons the process
- Traditional pay-per-success pricing model
Custom OTP Authentication (Bring Your Own):
- Pay-per-message - Charged immediately upon Step 2 activation
- Billing occurs regardless of verification outcome
Important: Send every code your user enters to Step 3 (POST /api/v1/transactions/verify/{_id}) when the user submits it, including a custom OTP that your own server also checks. A phone number that keeps receiving codes with none verified can be blocked from new codes.
A real send's response includes dispatched: true; dev-mode test activations omit dispatched.
Next Steps:
- Extract the
_idfrom the responsedataobject - Save this
_id- you'll need it for verification in Step 3 - Display your OTP input interface to the user
- Prepare to collect and verify the user's OTP input
Channel Delivery Confirmation
Error Handling: - 403 status codes: Indicate OTP delivery failures - 429 status codes: Rate limit exceeded - Akedly enforces a 60-second cooldown per PHONE NUMBER, not per account, so two accounts sending to the same number share it and one account's numbers do not compete. Wait for the time named in the message before retrying. Handle these error cases gracefully in your application flow.
Step 3: Verify User's OTP Input
Submit the user's OTP input for verification using the _id from Step 2:
- Name
- otp
- Type
- string
- Description
The OTP code entered by the user (must be sent as a string)
Successful Authentication Response
- Name
- status
- Type
- string
- Description
Authentication result status ("success")
- Name
- data
- Type
- JSON
- Description
Complete transaction data including
mainTransactionandtransactionReqobjects
- Name
- message
- Type
- string
- Description
Human-readable success message
- Name
- mainTransaction
- Type
- JSON
- Description
The original transaction object from Step 1, containing pipeline and user details
- Name
- transactionReq
- Type
- JSON
- Description
Verification attempt details including timestamps for OTP sending, user input, and verification completion
Note: The response data is provided for logging and analytics purposes. You can also view all verification attempts in your Akedly dashboard at https://app.akedly.io/admin/verifications.
Failed Authentication Response
- Name
- data
- Type
- JSON
- Description
Contains
frontendCallbackURLfor redirect handling (only relevant when using Akedly's hosted authentication interface)
- Name
- message
- Type
- string
- Description
Error description explaining why authentication failed
All error responses follow this format, with the message field providing specific details about what went wrong.
If you did not receive the verify response
Look the transaction up by its mainTransaction.transactionID instead of asking the user to try again. See Transaction status lookup. A lookup is the current result, not a copy of the verify response.
Error Reference
Common errors you may encounter when using the V1.0 REST API, grouped by category.
Authentication Errors
| Status | Error | Cause & Solution |
|---|---|---|
| 500 | User not found — Invalid API key | APIKey is missing, or the key is not currently usable because it is unknown, revoked, expired (past its grace period), or replaced. Verify that you are using a current API key from the Akedly dashboard. The response does not identify which condition applies. |
| 500 | Account not active | Your Akedly account is inactive. Contact support to reactivate. |
| 500 | Account not verified | Complete email verification for your Akedly account. |
| 403 | Activation window exceeded | Step 2 (activate) must be called within 2 minutes of Step 1 (create). That window is shorter than the 3-minute OTP validity which starts once the OTP is sent — do not queue activation for later. Create a new transaction instead. |
Configuration Errors
| Status | Error | Cause & Solution |
|---|---|---|
| 400 | INVALID_OTP_LANGUAGE | Invalid otpLanguage; see accepted values. V1 returns status, code and message. |
| 500 | No billing plan | Subscribe to a billing plan in your dashboard before creating transactions. |
| 500 | Subscription expired | Your billing subscription has expired. Renew in the dashboard. |
| 500 | Pipeline not found | The pipelineID does not exist. Verify the ID in your dashboard. |
| 500 | Pipeline inactive | The pipeline is disabled. Activate it in Dashboard > Pipelines. |
| 500 | Pipeline misconfigured | The pipeline is missing required settings (e.g., no verification methods enabled). Check pipeline configuration. |
Rate Limiting & Abuse Prevention
| Status | Error | Cause & Solution |
|---|---|---|
| 429 | Phone number banned | The phone number has been temporarily banned due to excessive failed attempts. Wait for the ban to expire. |
| 429 | Cooldown period | A 60-second cooldown is enforced between OTP requests to the same number. Wait and retry. |
| 429 | Throttle exceeded | Too many requests in a short period. Implement exponential backoff in your retry logic. |
Billing Errors
| Status | Error | Cause & Solution |
|---|---|---|
| 402 | Insufficient quota | Your message quota is exhausted. Top up your balance or upgrade your plan. |
Verification Errors
| Status | Error | Cause & Solution |
|---|---|---|
| 403 | Invalid OTP | The OTP entered by the user does not match. Prompt them to re-enter. |
| 403 | Transaction expired | The transaction has expired (3-minute TTL). Create a new transaction and resend the OTP. The body is {"message": "Transaction expired, please re-try from the beginning and request a new one"}. |
| 429 | MAX_ATTEMPTS_EXCEEDED | The 10th wrong OTP force-expires the transaction. Create a new one; further attempts return the 403 expired response above. |
| 403 | Transaction already verified | This transaction was already completed successfully. Each transaction can be verified once — create a new one for a new authentication. |
| 404 | Missing OTP or transaction request ID | The request omitted the otp field or the transaction request ID in the path. Both are required. |
Each transaction allows 10 OTP attempts, the same limit V1.2 and V2 enforce. Never retry more than that against one transaction — create a new one instead.
Telling the failures apart. The three 403s above share a status code, so branch on the message
field: an invalid OTP means "prompt the user to re-enter", while an expired or already-verified
transaction means "start a new transaction".
MAX_ATTEMPTS_EXCEEDED is the only coded V1 verification error, and the one difference from V1.2
worth knowing when you migrate. Its body carries a machine-readable code field, so you can
branch on it directly:
{
"status": "error",
"code": "MAX_ATTEMPTS_EXCEEDED",
"message": "Maximum OTP attempts exceeded...",
"data": { "frontendCallbackURL": "https://your-app.example.com/auth/callback" }
}
V1.2 returns a code on every error. V1's ordinary legacy failures remain message-only, but
coded exceptions include INVALID_OTP_LANGUAGE, MAX_ATTEMPTS_EXCEEDED, OTP template selection, and destination-policy
refusals. Preserve code whenever the response includes one and otherwise use message.
OTP Template Errors
Returned when the request or the pipeline default selects an OTP template that cannot be used. Nothing is sent or billed. Unlike the message-only errors above, these carry status and code. Remedies are in the OTP templates guide.
| Status | Code | Cause |
|---|---|---|
| 400 | TEMPLATE_UNKNOWN | The template id is malformed, not found, or not owned by the account. |
| 400 | TEMPLATE_ARCHIVED | The template was archived. |
| 400 | TEMPLATE_WABA_REQUIRED | The account has no active own WABA, or its token cannot be read. |
| 400 | TEMPLATE_WABA_CHANGED | The template belongs to a previous WABA. |
| 400 | TEMPLATE_NOT_APPROVED | The OTP language has no approved variant. |
| 400 | TEMPLATE_REJECTED | Meta rejected the OTP language variant. |
| 400 | TEMPLATE_VARIABLE_MISSING | A required variable has no request value or stored default. |
| 400 | TEMPLATE_VARIABLE_INVALID | A variable name or value is invalid, or the template object contains an unsupported key. |
Server Errors
| Status | Error | Cause & Solution |
|---|---|---|
| 500 | Failed to send OTP | Internal error during OTP delivery. Retry the request. If persistent, contact support. |
