📊
Total usage this month
0 / 1,000
Plan resets monthly
✉️
Email OTPs
0 / 200
Free tier · 200/month
📱
SMS OTPs
0 / 0
Upgrade to unlock SMS
Subscription
Loading...
Current plan
Free
Status
Active
Expires
Never
API Keys
Use your key in the X-API-Key header
Name Key Usage Created Status Webhook Actions
🔑

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):

{ "phone": "+971501234567", "requestId": "abc123def456", "status": "verified", "verifiedAt": "2026-04-04T14:30:00.000Z" }

Test payload (sent when you click the Test button):

{ "phone": "+000000000000", "requestId": "test_1712234567890", "status": "test", "verifiedAt": "2026-04-04T14:30:00.000Z" }

Field descriptions:

  • phonePhone number in E.164 format
  • requestIdUnique identifier per verification
  • statusEither 'verified' (live) or 'test'
  • verifiedAtISO 8601 timestamp of verification

Security: HMAC-SHA256 Signature Verification

Every webhook request includes an X-AkidOTP-Signature header. You must verify this signature before processing the webhook.

Your webhook secret:

••••••••••••••••

Signing algorithm:

HMAC-SHA256(your_webhook_secret, raw_JSON_body) === X-AkidOTP-Signature header

⚠️ 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)
// routes/api.php Route::post('/webhook/akidotp', function (Request $request) { $signature = $request->header('X-AkidOTP-Signature'); $rawBody = $request->getContent(); $secret = config('services.akidotp.webhook_secret'); $expected = hash_hmac('sha256', $rawBody, $secret); if (!hash_equals($expected, $signature)) { return response()->json(['error' => 'Invalid signature'], 401); } $data = json_decode($rawBody, true); if ($data['status'] === 'test') { return response('OK', 200); } $phone = $data['phone']; $requestId = $data['requestId']; // TODO: Update your database return response('OK', 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

  1. 1Generate an API key in your dashboard
  2. 2Click "Webhook" on the key → enter your HTTPS endpoint URL → Save
  3. 3Copy the auto-generated webhook secret → store it in your server's environment variables
  4. 4Implement one of the handler examples above on your server
  5. 5Click "Test" in the dashboard to verify your server receives and validates the webhook
  6. 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...

Notifications

🔔
No notifications yet