Developers
Developer guide
WHMCS module, REST API, verification status codes, HTTP errors, and sample clients for PHP, Python, Node.js, and cURL.
WHMCS module
Download the Identity Verification addon
Extract the zip into your WHMCS tree as modules/addons/identityverification/. Documents and selfies stay on this platform. WHMCS stores status only.
- Download the module and copy the folder into WHMCS.
- Activate it under System Settings → Addon Modules.
- Set API Base URL to
https://www.kyc.af/api/v1. - Paste an organization API key and the webhook signing secret.
- Set the webhook URL on this platform to
https://your-whmcs.example/modules/addons/identityverification/webhook.php. - Enable Require IDV on the products or TLDs that need it.
Authentication
Every request uses a Bearer organization API key. Create keys on the API keys page. The secret is shown once.
Authorization: Bearer kyc_live_... Content-Type: application/json Base URL: https://www.kyc.af/api/v1
API access is available after the organization is approved. Optional IP allow lists on a key return 403 from other addresses.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /verifications | Create a verification. Reuses the same reference_id if it already exists. |
| GET | /verifications | List verifications. Optional status and per_page (1–100). |
| GET | /verifications/{id} | Fetch one verification by public ID (ver_...). |
| POST | /verifications/{id}/cancel | Cancel an in-progress verification. |
| GET | /users/{id}/verification-status | Latest verification for a user public ID. |
Create body
| Field | Required | Notes |
|---|---|---|
| Yes | Customer email. | |
| customer_name | No | Customer full name. |
| reference_id | No | Your own ID. Omit to let the API generate a REF-... value. The same value returns the existing record. |
| phone | No | Customer phone. |
| callback_url | No | Per-verification webhook URL. Must be an allowed public HTTPS host. Defaults to the organization webhook URL. |
| return_url | No | Where to send the customer after they finish. |
Verification status codes
The API status field uses these values. Webhook events are verification.{status}.
| Code | Label | Final | Meaning |
|---|---|---|---|
| created | Created | No | Verification was created. Send the customer the verification_url. |
| pending | Pending | No | The customer opened the link and accepted consent. |
| document_submitted | Document in progress | No | The customer started uploading an identity document but has not finished. |
| selfie_required | Selfie required | No | A selfie is still required to continue. |
| processing | Processing | No | Document and face checks are running. |
| pending_review | Pending review | No | Automated checks finished. A reviewer must decide. |
| additional_document_required | Additional document | No | The reviewer asked for another document. |
| verified | Verified | Yes | Identity was approved. Result fields are available on the API. |
| rejected | Rejected | Yes | Identity was rejected. This status is final. |
| expired | Expired | Yes | The customer did not finish before the link expired. |
| cancelled | Cancelled | Yes | The verification was cancelled by the organization. |
Identity fields in result are only filled when status is verified.
HTTP status codes
| HTTP | When |
|---|---|
| 200 | Success, including a create that reused an existing reference_id. |
| 201 | A new verification was created. |
| 401 | Missing or invalid API key. API key required. or Invalid API key. |
| 403 | Organization is not active, API access is suspended, or the request IP is not allowed. |
| 404 | Verification not found for this organization. |
| 422 | Validation failed. Check the errors object. |
| 429 | Rate limited. Default is 60 authenticated requests per minute. |
| 500 | Unexpected server error. |
Webhooks
KYC POSTs JSON to the organization webhook URL, or to callback_url when you send one. Failed deliveries retry up to five times.
{
"event": "verification.verified",
"data": {
"verification_id": "ver_01xyz",
"reference_id": "WHMCS-CLIENT-125",
"status": "verified",
"verified_at": "2026-09-07T15:30:00+00:00"
}
}
Every event includes verification_url. When more evidence is needed, the payload also includes additional_document_url, and verification_url is that same additional-document link. Do not send the customer the original /v/... URL after additional_document_required.
{
"event": "verification.additional_document_required",
"data": {
"verification_id": "ver_01xyz",
"reference_id": "WHMCS-CLIENT-125",
"status": "additional_document_required",
"verified_at": null,
"verification_url": "https://kyc.af/additional/TOKEN",
"additional_document_url": "https://kyc.af/additional/TOKEN"
}
}
Verify HMAC-SHA256 of timestamp.body using the webhook secret. Compare with X-Webhook-Signature (also sent as X-Hub-Signature-256). The timestamp is in X-Webhook-Timestamp. Reject signatures older than five minutes and never reuse a nonce.
Sample code
Create a verification, then verify an incoming webhook. Replace the API key and webhook secret.
$base = 'https://www.kyc.af/api/v1';
$key = 'kyc_live_YOUR_KEY';
$ch = curl_init($base.'/verifications');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer '.$key,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'customer_name' => 'Ahmad Khan',
'email' => 'customer@example.com',
'reference_id' => 'ORDER-125',
'return_url' => 'https://your-app.example/clientarea.php',
]),
]);
$response = curl_exec($ch);
curl_close($ch);
$verification = json_decode($response, true);
echo $verification['verification_url'];
// Webhook receiver
$secret = 'YOUR_WEBHOOK_SECRET';
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid webhook signature.');
}
$event = json_decode($body, true);
if (($event['data']['status'] ?? '') === 'verified') {
// Mark the customer verified in your system.
}
import hashlib
import hmac
import json
import os
import requests
BASE = "https://www.kyc.af/api/v1"
KEY = "kyc_live_YOUR_KEY"
response = requests.post(
f"{BASE}/verifications",
headers={"Authorization": f"Bearer {KEY}", "Accept": "application/json"},
json={
"customer_name": "Ahmad Khan",
"email": "customer@example.com",
"reference_id": "ORDER-125",
"return_url": "https://your-app.example/clientarea.php",
},
timeout=15,
)
response.raise_for_status()
verification = response.json()
print(verification["verification_url"])
# Webhook receiver (Flask)
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["KYC_WEBHOOK_SECRET"]
@app.post("/webhooks/kyc")
def kyc_webhook():
body = request.get_data()
timestamp = request.headers.get("X-Webhook-Timestamp", "")
signature = request.headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(
SECRET.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
event = json.loads(body)
if event.get("data", {}).get("status") == "verified":
pass
return {"ok": True}
import crypto from "node:crypto";
const BASE = "https://www.kyc.af/api/v1";
const KEY = "kyc_live_YOUR_KEY";
const created = await fetch(`${BASE}/verifications`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
customer_name: "Ahmad Khan",
email: "customer@example.com",
reference_id: "ORDER-125",
return_url: "https://your-app.example/clientarea.php",
}),
}).then((response) => response.json());
console.log(created.verification_url);
// Webhook receiver (Express)
import express from "express";
const app = express();
app.use(express.raw({ type: "application/json" }));
app.post("/webhooks/kyc", (req, res) => {
const timestamp = req.get("X-Webhook-Timestamp") ?? "";
const signature = req.get("X-Webhook-Signature") ?? "";
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.KYC_WEBHOOK_SECRET)
.update(`${timestamp}.`)
.update(req.body)
.digest("hex");
const valid =
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!valid) {
return res.status(401).send("Invalid webhook signature.");
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.data?.status === "verified") {
// Mark the customer verified in your system.
}
res.json({ ok: true });
});
curl -X POST "https://www.kyc.af/api/v1/verifications" \
-H "Authorization: Bearer kyc_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"customer_name": "Ahmad Khan",
"email": "customer@example.com",
"reference_id": "ORDER-125",
"return_url": "https://your-app.example/clientarea.php"
}'
curl "https://www.kyc.af/api/v1/verifications/ver_01xyz" \
-H "Authorization: Bearer kyc_live_YOUR_KEY" \
-H "Accept: application/json"
Ready to integrate?
Create an account to get API keys once your organization is approved.