Developer Documentation

Build with Bangladesh's identity infrastructure

A unified REST API for identity, biometric, business, and telecom verification — designed for fintechs, marketplaces, telecom, and regulated SaaS.

Platform Overview

KYC.bd is a modular verification stack for Bangladesh and emerging markets. Each module exposes a focused REST endpoint over HTTPS with JSON payloads.

Identity Verification

National ID workflows with consent capture.

Face Match Engine

Selfie-to-document biometric comparison & liveness.

Business Verification

Company registration, TIN, and merchant checks.

Telecom Verification

SIM ownership and operator validation.

Webhooks

Real-time event delivery for verification lifecycle.

API Gateway

Rate limits, audit logs, and unified auth.

Authentication & base URL

The verification API is served from https://kyc.bd/api/public/v1. Every request is authenticated with a project API key in the Authorization header. Create keys in the verification console.

HTTP
Authorization: Bearer kyc_test_xxxxxxxxxxxxxxxx
Test mode

kyc_test_…

Runs against the deterministic test provider. Set template.mock_outcome to exercise every path.

Live mode

kyc_live_…

Every submitted live session goes to the human review queue (requires_review). A trained reviewer inspects the documents and selfie, and their approve/reject is the final decision, sent as a verification.completed / verification.failed webhook.

Keys are scoped to one project. Only a hash is stored; a key is shown once at creation. Revoked or expired keys return 401.

Errors & request IDs

Every response carries an x-request-id header. Errors share one envelope:

Error
HTTP/1.1 409 Conflict
x-request-id: req_3f9c1a7e2b8d4c6f0a1e5b7d

{
  "error": {
    "code": "invalid_state",
    "message": "Cannot move verification from verified to cancelled.",
    "request_id": "req_3f9c1a7e2b8d4c6f0a1e5b7d"
  }
}
StatusCodeMeaning
400invalid_json / idempotency_key_requiredMalformed body or missing Idempotency-Key
401unauthorizedMissing, revoked or expired API key
404not_foundResource does not exist in this project
409invalid_stateAction not allowed in the current status
422validation_error / missing_document / missing_selfieField validation or missing uploads
422consent_requiredApplicant has no active identity_verification consent
422document_too_large / document_type_mismatch / document_missingUpload over 10 MB, bytes don't match declared type, or object not found — re-upload
422invalid_urlWebhook URL rejected (private/reserved address, non-public host, credentials, or non-443 port in live)
500internal_errorUnexpected error — retry with the same Idempotency-Key

Idempotency

POST /verifications requires an Idempotency-Key header (max 128 characters). Replaying the same key within the same project returns the original verification with 200 instead of creating a new one — safe for network retries. POST /applicants is idempotent on external_id. Webhook deliveries carry kyc-event-id and idempotency-key headers so your receiver can de-duplicate.

cURL
curl -X POST https://kyc.bd/api/public/v1/verifications \
  -H "Authorization: Bearer $KYC_API_KEY" \
  -H "Idempotency-Key: onboarding-USER-1001-attempt-1" \
  -H "Content-Type: application/json" \
  -d '{"applicant_id":"5b0c…","template":{"documents":["nid"],"require_selfie":true}}'

Verification lifecycle

Status changes are enforced by the platform; illegal transitions return 409 invalid_state.

States
created ──► started ──► documents_submitted ──► processing ──┬─► verified
   │           │                 ▲                          ├─► failed
   │           │                 │                          ├─► requires_review ──► verified | failed | requires_retry
   │           │                 └──── requires_retry ◄─────┘
   └───────────┴──► cancelled | expired (7 days by default)

Decisions come from a per-project risk policy: approve, review, reject or retry. risk_score is policy output from configured signal weights, not an objective measure of identity. In test mode, template.mock_outcome accepts pass, review, retry or fail. To pin a published flow version, send flow_id at the top level of the request; template.flow_id is rejected with 422.

Endpoint reference

MethodPathPurpose
GET/meCurrent project, organization and key
GET/projectsProjects in the organization
GET/usageBillable usage totals, last 30 days
POST/applicantsCreate applicant with consent (idempotent on external_id)
GET/applicants/{id}Fetch applicant (no personal data returned)
POST/applicants/{id}/consentsRecord consent { purpose, consent_version }
GET/applicants/{id}/consentsConsent history with active flag
POST/applicants/{id}/consents/{consentId}/withdrawWithdraw consent; cancels open verifications and erases personal data
POST/applicants/{id}/deletionRequest erasure of all identity data (async, emits applicant.deleted)
POST/verificationsCreate verification (Idempotency-Key required)
GET/verificationsList verifications (?status=)
GET/verifications/{id}Fetch verification
POST/verifications/{id}/cancelCancel an open verification
POST/verifications/{id}/documentsGet a signed upload URL for one file
POST/verifications/{id}/submitSubmit uploads and run checks
GET/verifications/{id}/checksIndividual check results
GET/verifications/{id}/decisionRisk assessment, signals and review status
POST/webhook-endpointsRegister a webhook endpoint (secret shown once)
GET/webhook-endpointsList endpoints
GET/webhook-deliveriesRecent delivery attempts

1. Create an applicant

POST/applicants
Request
{
  "external_id": "USER-1001",
  "type": "individual",
  "consent": { "purpose": "identity_verification", "version": "2026-10" },
  "pii": { "full_name": "Applicant Name", "date_of_birth": "1990-01-01", "national_id": "1234567890" }
}
201 Created
{ "object": "applicant", "id": "5b0c2f1e-…", "external_id": "USER-1001", "type": "individual", "status": "active", "created_at": "2026-10-07T20:00:00Z" }

Personal data is stored in a restricted zone and is never returned by the API. POST /verifications requires an active identity_verification consent (inline here or via POST /applicants/{id}/consents), otherwise it returns 422 consent_required.

Retention: documents are purged after the project's document window (default 90 days); personal data after the PII window (default 365 days) once no verification is open; raw provider responses with documents. Deletion requests are processed within the hour.

2. Create a verification

POST/verifications
Request
{
  "applicant_id": "5b0c2f1e-…",
  "type": "individual_kyc",
  "country": "BD",
  "template": { "documents": ["nid"], "require_selfie": true, "mock_outcome": "pass" }
}
201 Created
{
  "id": "a581d10c-…", "object": "verification", "applicant_id": "5b0c2f1e-…",
  "status": "created", "decision": null, "risk_score": null, "risk_level": null,
  "expires_at": "2026-10-14T20:00:00Z", "created_at": "2026-10-07T20:00:00Z"
}

3. Upload documents

POST/verifications/{id}/documents

Request a short-lived signed upload URL per file. document_type: nid, passport, driving_license or selfie. Accepted: JPEG, PNG, WebP, PDF up to 10 MB.

Request → response
POST /verifications/a581d10c-…/documents
{ "document_type": "nid", "side": "front", "mime_type": "image/jpeg", "file_size": 284113 }

201 Created
{ "object": "document_upload", "document_id": "9e1d…", "upload_url": "https://…signed…",
  "method": "PUT", "headers": { "content-type": "image/jpeg" }, "expires_in": 7200 }
Upload the file
curl -X PUT "$UPLOAD_URL" -H "Content-Type: image/jpeg" --data-binary @nid-front.jpg

4. Submit for checks

POST/verifications/{id}/submit

Fingerprints the uploaded files, then runs document, liveness and face-match checks and the risk policy.

202 Accepted
{ "id": "a581d10c-…", "status": "verified", "decision": "approve", "risk_score": 0, "risk_level": "low", "documents_received": 2 }

5. Read results

GET/verifications/{id}/decision
200 OK
{
  "object": "decision", "verification_id": "a581d10c-…", "status": "requires_review", "decision": "review",
  "risk": { "risk_score": 40, "risk_level": "medium", "decision": "review",
            "risk_signals": [{ "code": "face_match_inconclusive", "weight": 40, "detail": "score 0.72 < 0.8" }] },
  "review": { "id": "…", "status": "open", "resolved_at": null }
}

Module previews: Identity Verification

The module examples below describe planned product surfaces. The live, generally available API is the verification API documented above.

POST/identity/verify
Request
{
  "full_name": "John Doe",
  "date_of_birth": "1998-05-15",
  "national_id": "1234567890",
  "mobile": "+8801XXXXXXXXX",
  "consent": true,
  "reference_id": "USER-1001"
}
Response
{
  "status": "verified",
  "verification_id": "KYC-8472931",
  "match_score": 98.7,
  "risk_level": "low",
  "timestamp": "2026-05-07T12:00:00Z"
}
cURL
curl -X POST https://api.kyc.bd/v1/identity/verify \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "full_name":"John Doe",
    "date_of_birth":"1998-05-15",
    "national_id":"1234567890",
    "mobile":"+8801XXXXXXXXX",
    "consent":true
  }'

Face Match

POST/biometric/face-match
Request
{
  "document_image": "base64_encoded_image",
  "selfie_image": "base64_encoded_image",
  "reference_id": "USER-1001"
}
Response
{
  "status": "matched",
  "confidence_score": 96.4,
  "liveness": true,
  "fraud_risk": "low"
}

Business Verification

POST/business/verify
Request
{
  "company_name": "Example Technologies Ltd",
  "registration_number": "C-123456",
  "tin": "987654321",
  "contact_email": "admin@example.com"
}
Response
{
  "status": "verified",
  "company_status": "active",
  "risk_level": "medium",
  "verification_id": "BIZ-82711"
}

Telecom Verification

POST/telecom/verify
Response
{
  "mobile_verified": true,
  "operator": "Detected Operator",
  "sim_status": "active",
  "risk_score": 12
}

Webhooks

Register an endpoint with POST /webhook-endpoints ({"url": "https://…", "events": ["*"]}). The signing secret (whsec_…) is returned once. Live endpoints must use HTTPS on port 443 with a public hostname; every A/AAAA record is resolved at registration and again before each delivery, and private, loopback, link-local, CGNAT, metadata and other reserved ranges are refused. Redirects are never followed (a 3xx counts as a failed attempt), each attempt aborts after 10 seconds, and at most 64 KB of your response body is read. Residual risk: a DNS answer can change between our check and the connection (resolve-then-fetch TOCTOU) because the runtime cannot pin an IP; Cloudflare's platform egress independently blocks RFC1918 and cloud-metadata targets, and a dedicated egress proxy is planned.

EventDescription
verification.createdVerification session created
verification.startedFirst document upload URL issued
verification.document_submittedDocuments submitted for checks
verification.processingChecks are running
verification.requires_retryApplicant must re-upload (e.g. unreadable document)
verification.requires_reviewSent to the manual review queue
verification.completedVerified (approved by policy or reviewer)
verification.failedRejected by policy or reviewer
verification.expiredNot completed before expires_at
verification.cancelledCancelled via the API, consent withdrawal or deletion request
applicant.deletedDeletion request completed: documents, personal data and raw provider data erased
Delivery
POST https://your-app.example/webhooks/kyc
content-type: application/json
kyc-signature: t=1791403200,v1=5d41402abc4b2a76b9719d911017c592…
kyc-event-id: 7c9e6679-7425-40de-944b-e07fc1f90ae7
idempotency-key: 7c9e6679-7425-40de-944b-e07fc1f90ae7

{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "type": "verification.completed",
  "created_at": "2026-10-07T20:00:05Z",
  "data": { "id": "a581d10c-…", "object": "verification", "status": "verified", "decision": "approve", "risk_score": 0 }
}

Verify the signature: compute HMAC-SHA256 of `${t}.${rawBody}` with your secret, compare to v1 in constant time, and reject timestamps older than 5 minutes.

Node.js
import crypto from "node:crypto";

export function verifyKycSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}
Python
import hmac, hashlib, time

def verify_kyc_signature(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Retries: any non-2xx response or a 10-second timeout is retried with exponential backoff (30s, 60s, 2m, 4m, 8m) for up to 6 attempts, then marked failed. Inspect deliveries with GET /webhook-deliveries.

SDKs & Developer Tools

Official client libraries with typed methods and built-in retries.

Node.js
PHP
Python
Laravel
Java
Go

Compliance & Security

Infrastructure

  • TLS 1.3 encrypted API communication
  • Enterprise-grade API gateway
  • Audit logging and immutable activity records
  • Role-based access control
  • Rate limiting and anomaly monitoring

Compliance

  • PDPA-aligned data handling workflows
  • Consent-driven verification architecture
  • KYC/AML operational support
  • Enterprise audit support
  • Regulatory response readiness

Ready to integrate?

Get sandbox credentials and start verifying users today.