No API keys yet — generate your first key to get started
📘Webhook Integration Guide
How Webhooks Work
When a user successfully verifies their identity (via WhatsApp, Telegram, Email, or SMS), AkidOTP
automatically sends a POST request to your server with the verification details.
User Verifies
→
AkidOTP Confirms
→
POST to Your Server
→
You Process
Webhook failure never blocks verification — the user still receives their OTP even if your server is
unreachable.
Payload Reference
Live verification payload (sent on successful
verification):
⚠️ CRITICAL: Use the RAW request body string for verification, NOT a re-serialized object. Re-serializing
may change whitespace and cause signature mismatch.
Code Examples — Working
Handlers
const crypto = require('crypto');
const express = require('express');
const app = express();
// IMPORTANT: Use raw body for signature verification
app.post('/webhook/akidotp', express.json({
verify: (req, res, buf) => { req.rawBody = buf; }
}), (req, res) => {
const signature = req.headers['x-akidotp-signature'];
const expected = crypto
.createHmac('sha256', process.env.AKIDOTP_WEBHOOK_SECRET)
.update(req.rawBody)
.digest('hex');
if (signature !== expected) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// Signature valid — process the verification
const { phone, requestId, status, verifiedAt } = req.body;
if (status === 'test') {
console.log('Test webhook received successfully');
return res.status(200).send('OK');
}
console.log(`User ${phone} verified at ${verifiedAt}`);
// TODO: Update your database, mark user as verified, etc.
res.status(200).send('OK');
});
app.listen(3000);
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_here" # From AkidOTP dashboard
@app.route('/webhook/akidotp', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-AkidOTP-Signature', '')
raw_body = request.get_data(as_text=True)
expected = hmac.new(
WEBHOOK_SECRET.encode(),
raw_body.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify({"error": "Invalid signature"}), 401
data = json.loads(raw_body)
if data.get('status') == 'test':
print('Test webhook received successfully')
return 'OK', 200
phone = data.get('phone')
request_id = data.get('requestId')
print(f'User {phone} verified (request: {request_id})')
# TODO: Update your database
return 'OK', 200
if __name__ == '__main__':
app.run(port=3000)
# views.py
import hmac
import hashlib
import json
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
@csrf_exempt
def akidotp_webhook(request):
if request.method != 'POST':
return HttpResponse(status=405)
signature = request.headers.get('X-AkidOTP-Signature', '')
raw_body = request.body.decode('utf-8')
expected = hmac.new(
settings.AKIDOTP_WEBHOOK_SECRET.encode(),
raw_body.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return JsonResponse({"error": "Invalid signature"}, status=401)
data = json.loads(raw_body)
if data.get('status') == 'test':
return HttpResponse('OK', status=200)
phone = data.get('phone')
request_id = data.get('requestId')
# TODO: Mark user as verified in your database
return HttpResponse('OK', status=200)
# Simulate what AkidOTP sends to your server:
SECRET="your_webhook_secret_here"
BODY='{"phone":"+971501234567","requestId":"test_123","status":"test","verifiedAt":"2026-04-04T14:30:00.000Z"}'
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://yourapp.com/webhook/akidotp \
-H "Content-Type: application/json" \
-H "X-AkidOTP-Signature: $SIGNATURE" \
-d "$BODY"
Response Codes &
Retry Policy
Your Server Returns
AkidOTP Behavior
200–299
✅ Delivery successful. Status saved as success.
300–499
❌ Delivery failed. One retry in 5 seconds.
500–599
❌ Delivery failed. One retry in 5 seconds.
Timeout (>10s)
❌ Delivery failed. One retry in 5 seconds.
Connection refused
❌ Delivery failed. One retry in 5 seconds.
After the single retry fails, status is saved as failed on the API
key. Webhook failure never affects the verification — the user still gets their OTP.
Security Best Practices
Always verify the signature before processing
Use hmac.compare_digest()
(Python) or constant-time comparison to prevent timing attacks
Only accept HTTPS — AkidOTP will never send to
HTTP
Store your webhook secret in environment
variables, never in code
Return 200 quickly — do
heavy processing asynchronously
Idempotency: use requestId to
deduplicate in case of retries
Quick Setup Checklist
1Generate an API key in your dashboard
2Click "Webhook"
on the key → enter your HTTPS endpoint URL → Save
3Copy the auto-generated
webhook secret → store it in your server's environment variables
4Implement one of the handler examples above on your
server
5Click "Test" in
the dashboard to verify your server receives and validates the webhook
6Start sending verifications — webhooks fire
automatically
Need more verifications?
Upgrade to Starter for 10,000 email OTPs + SMS — just $9/month
Support
Submit a ticket or view your history
New
support ticket
Your tickets
Loading tickets...
Ticket Detail
New API key
Give your key a name so you can identify it later
🔗 Configure Webhook
AkidOTP will POST to your server every time a user verifies. Use the secret to verify the signature.
100,000
verifications/month · Everything in Starter + SMS
$29/mo
Enterprise
Unlimited · Custom SLA · White-label
Contact us
Tell us about your needs
Accepted Payment Methods
BTC
USDT
ADA
Secure · No card data stored · Crypto verified
on-chain
Notifications
🔔
No notifications yet
₿
Pay with Cryptocurrency
Loading available methods…
⚠️ Crypto payments are non-refundable. You must send the exact amount shown — any other amount cannot be
automatically detected and may be lost. By proceeding, you agree to these terms.
Send Payment
30:00
⚠️
IMPORTANT: Send the EXACT amount shown below. Any different amount will not be detected and cannot be
refunded.
Send exactly this amount
To this wallet address
Please confirm you have read and agree to the terms
above.
⏳ Waiting for blockchain
confirmation… This page updates automatically every 5 seconds.