Skip to content

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.

  1. Download the module and copy the folder into WHMCS.
  2. Activate it under System Settings → Addon Modules.
  3. Set API Base URL to https://www.kyc.af/api/v1.
  4. Paste an organization API key and the webhook signing secret.
  5. Set the webhook URL on this platform to https://your-whmcs.example/modules/addons/identityverification/webhook.php.
  6. 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
email 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.

Create Organization Account