Documentation

Ship identity verification, faster.

Kaluta KYC gives you one API for document verification, facial biometrics, liveness detection, and webhooks. Follow the quickstart or jump straight to the reference.

Overview

Kaluta KYC is an identity verification API. Your backend creates a session per end user, then your frontend opens that session in one of three ways:

  • Embedded widget runs inline on your site in a modal. No redirect.
  • Full page redirect hands the user to a hosted page and back.
  • Mobile handoff shows a QR code on desktop. Your user finishes on their phone.

The final decision and extracted data arrive on your server through a signed webhook.

Setup

  1. Create a business account. You get 5 free verifications to start.
  2. Open Dashboard → API Keys and generate a key. The full key is shown once. Store it as a secret on your server.
  3. Configure at least one webhook in Dashboard → Webhooks pointing to your backend.

Keep funds on your account from Dashboard → Billing. Pay as you go at $1.00 per completed verification.

Quickstart

From zero to first verification in three steps.

1. Create a session (server side)

bash
curl -X POST https://kalutakyc.com/v1/sessions \
  -H "X-API-Key: klt_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "external_id": "user_42" }'

# → { "session_id": "abc123",
#     "verification_url": "https://kalutakyc.com/verify/abc123",
#     "expires_at": "2026-06-02T00:00:00Z" }

2. Open the verifier (client side)

html
<script src="https://kalutakyc.com/embed.js"></script>
<button onclick="open()">Verify my identity</button>
<script>
  function open() {
    KalutaKYC.open({
      url: "https://kalutakyc.com/verify/abc123",
      onComplete: (r) => console.log(r.status, r.score),
    });
  }
</script>

3. Handle the webhook (server side)

javascript
app.post("/hooks/kaluta", (req, res) => {
  if (!isSignatureValid(req)) return res.status(401).end();
  if (req.body.event === "session.approved") {
    // unlock the user account
  }
  res.sendStatus(200);
});

Authentication

All server to server requests authenticate with your secret API key in theX-API-Key header. Dashboard endpoints use a JWT inAuthorization: Bearer. Keys start withklt_ and are bcrypt hashed in storage. The raw value is shown only at creation time.

http
X-API-Key: klt_live_xxxxxxxxxxxxxxxxxxxxxxxx

Never ship your API key to the browser

Always proxy session creation through your own backend. Anything visible in DevTools is public.

Create a session

Call this from your backend when a user needs to be verified.

json
POST /v1/sessions
X-API-Key: klt_live_xxx

{
  "external_id":   "user_42",
  "redirect_url":  "https://yourapp.com/kyc/done",
  "webhook_url":   "https://yourapp.com/hooks/kyc",
  "metadata":      { "plan": "premium" },

  "first_name":    "Jean Baptiste",
  "last_name":     "Kouame",
  "date_of_birth": "1992-03-15",
  "country":       "CI"
}
json
{
  "session_id":       "abc123",
  "verification_url": "https://kalutakyc.com/verify/abc123",
  "expires_at":       "2026-06-02T00:00:00Z"
}

Expected identity — strongly recommended

Provide the identity you already know (first_name, last_name, date_of_birth, country). Our AI extracts the same fields from the ID document and cross-checks them: any mismatch on a field you supplied causes an automatic rejection, with the failing field named in the webhook. The country field accepts an ISO code or a plain country name ("CI", "CIV", and "Côte d'Ivoire" are all equivalent) and is matched against both the issuing country and the holder's nationality on the document.

Document type is detected automatically

You no longer need to send document_type. The user can pick "Detect automatically" and our vision AI recognizes passports, national IDs, driver licenses, and residence permits from the photo. You can still pass an explicit type to lock the flow to one document kind.

Sessions expire 24 hours after creation. Billing: one charge covers a block of attempts (3 by default) — see Retries below.

Retries

When a verification is rejected, the end user can immediately start again — the rejection screen shows a retry button that spawns a fresh session inheriting your external_id, webhook, redirect URL, and expected identity. You can also trigger it server side:

bash
POST /v1/sessions/{rejected_session_id}/retry
# → { "session_id": "def456", "verification_url": "...", "retry_index": 1 }

Retries are unlimited. You are charged once per block of attempts (default: 1 charge covers 3 attempts — attempt 4 opens a new paid block, and so on). Each new session in the chain fires its own session.created webhook and carries metadata._retry_index.

Retrieve a session

Full session with extracted identity, scores, and short lived signed image URLs.

bash
GET /v1/sessions/{id}
Authorization: Bearer <jwt>

Session lifecycle

A session moves through these states. Use webhook events to track them.

StatusMeaning
createdSession created. User has not started yet.
document_submittedDocument uploaded. AI extraction and cross-checks ran.
face_submittedSelfie uploaded and face match ran.
approvedAutomated decision: approved. Webhooks fired.
rejectedAutomated decision: rejected, with a plain-language reason. Webhook fired.
expired24 hour window elapsed without completion.

Decisions are fully automated — no manual review step. A session is approved when the combined score reaches the threshold and every hard check passes (face matches the document, extracted identity matches what you supplied, liveness if enabled). Otherwise it is rejected immediately with the weakest signal named in rejection_reason, and the user can retry.

Verification steps configuration

Document scan + face match always run. Three optional steps are toggled per business in Dashboard → Settings → Verification steps: Proof of address (the user photographs a utility bill or bank statement; we extract the holder name and address and match them against the ID) and Liveness check (anti-spoofing on the selfie), and Document security check (hologram verification, see above). Settings are snapshotted onto each session at creation, so in-flight sessions keep the flow they started with.

Embedded widget

Run the flow inline on your website in a secure modal. Your users never leave your page.

html
<script src="https://kalutakyc.com/embed.js"></script>

<button id="verify-btn">Verify my identity</button>
<script>
  document.getElementById("verify-btn").addEventListener("click", () => {
    KalutaKYC.open({
      url: "https://kalutakyc.com/verify/abc123",
      onReady:    ()  => console.log("widget loaded"),
      onStep:     (s) => console.log("step:", s),
      onComplete: (r) => console.log(r.status, r.score),
      onClose:    ()  => console.log("widget closed"),
    });
  });
</script>

KalutaKYC.open options

OptionTypeDescription
urlstringThe verification_url returned by POST /v1/sessions.
sessionIdstringAlternative to url. Combined with baseUrl.
baseUrlstringOverride the app host. Defaults to https://kalutakyc.com.
onReady() => voidFires when the widget iframe has loaded.
onStep(step: string) => voidFires on every step change.
onComplete({status, score}) => voidFires when the final result is known. Always treat the webhook as source of truth.
onClose(result?) => voidFires when the modal closes.

Redirect flow

Prefer a full page hand off? Send the user to theverification_url. When they finish, they are returned to yourredirect_url.

javascript
window.location.href = verificationUrl;
// or
KalutaKYC.redirect(verificationUrl);

Callback parameters

The verification outcome is appended to your redirect URL as query parameters, so your page can react immediately:

text
https://yourapp.com/kyc/done
  ?kyc_session_id=abc123
  &kyc_status=approved        // approved | rejected
  &kyc_external_id=user_42    // your own reference, if you set one

Never trust the URL alone

Query parameters can be edited by the user. Treat them as a UX hint only — update your UI optimistically, but confirm the real result server side via the webhook payload (which carries the full extracted identity and scores) or a GET /v1/sessions/{id} call. Personal data (names, birth dates) is intentionally never placed in the URL.

Mobile handoff

Desktop users often have low quality webcams or no rear camera. The verification flow includes a built in handoff. On the welcome screen and in every camera error fallback, the user can click Continue on my phone. A QR code appears. They scan it with their phone camera and finish the document and selfie there.

No integration work needed. The desktop page polls the session status and automatically advances to the final screen when the phone is done.

Document security check

Optional anti forgery step. The user tilts their ID in front of the camera while we capture a burst of frames, then we analyse how the surface reacts to the light. Genuine identity documents carry optically variable devices — holograms, kinegrams, colour shifting ink — whose highlights travel across the card and change hue as the angle changes. A photocopy reflects flatly and a phone screen adds its own artefacts, so neither can fake the effect.

What it blocks

Photocopies and laser prints, photos of an ID displayed on another screen, and cards whose security overlay was removed or reprinted. It runs locally on our servers, so it adds no per check cost and no third party sees your users' documents.

Turning it on

Enable Document security check under Dashboard → Settings → Verification steps, or set the flag through the API:

json
PUT /v1/auth/me
Authorization: Bearer <jwt>

{ "require_document_video": true }

As with every step toggle, the setting is snapshotted onto each session at creation, so sessions already in flight keep the flow they started with.

Signals we compute

SignalWhat it measures
highlight_travelHow far the bright reflection moves between frames. A hologram sweeps across the card, printed glare barely moves.
iridescenceHue instability inside the highlight. Diffraction throws different wavelengths at different angles; paper glare stays white.
highlight_area_variationHow much the lit area swells and shrinks as the card turns.
screen_replay_likelihoodPeriodic energy in the frequency domain, the moire signature of filming a display.

The four signals blend into doc_video_score (0-100), returned on the session and in webhook payloads alongside the raw values. A session is rejected outright when the document shows no optically variable behaviour at all.

Uploading the capture yourself

Building your own UI? Post the frames, or a short video file, to the session. Send at least four frames covering a slow tilt.

bash
POST /v1/sessions/{id}/document-video
Content-Type: multipart/form-data

files=@frame_01.jpg
files=@frame_02.jpg
files=@frame_03.jpg
...
# or a single clip: files=@tilt.mp4

Duplicate document detection

Independently of the security check, every document we read is fingerprinted from its number, surname and date of birth. If the same physical document later appears in a verification for a different person, the new session is rejected and carries duplicate_of_session_id pointing at the original approval. The same person re-verifying with the same ID is never penalised.

Business verification (KYB)

KYB verifies a company rather than a person. You create a session, the company representative uploads registration documents on a hosted page, and our vision AI extracts the legal identity of the entity: legal name, registration number, incorporation date, registered address, company status and directors. The decision is automatic, exactly like KYC.

KYB and KYC compose naturally: verify the company, verify its legal representative, then link the two by passing the representative's KYC session id when creating the KYB session.

Accepted documents

TypeNotes
certificate_of_incorporationFounding document. Identifies the company on its own.
registration_extractRCCM, Kbis, Companies House extract. Identifies the company on its own.
articles_of_associationStatutes or bylaws. Strengthens the file.
proof_of_addressRecent bill or lease for the registered office.
ownership_structureShareholding chart or list of beneficial owners.
tax_certificateTax identification or good standing certificate.

At least one of the two first types is required to submit a file. PDFs and photos are both accepted, up to 20 MB per document.

Create a KYB session

json
POST /v1/kyb/sessions
X-API-Key: klt_live_xxx

{
  "external_id":  "company_42",
  "redirect_url": "https://yourapp.com/kyb/done",
  "webhook_url":  "https://yourapp.com/hooks/kyb",
  "metadata":     { "tier": "enterprise" },

  "legal_name":          "ACME TRADING SARL",
  "registration_number": "CI-ABJ-2019-B-12345",
  "country":             "CI",

  "representative_session_id": "abc123"
}
json
{
  "session_id":       "kyb_abc123",
  "verification_url": "https://kalutakyc.com/kyb/kyb_abc123",
  "expires_at":       "2026-08-11T00:00:00Z"
}

Send the representative to verification_url. As with KYC, any legal_name, registration_number or country you supply is cross-checked against the documents, and a mismatch rejects the file automatically.

KYB sessions last 7 days

Companies often need to dig up paperwork, so KYB links live far longer than the 24 hour KYC window. One verification is billed at session creation.

Upload documents from your own UI

Prefer to build your own upload experience? Post directly to the session:

bash
POST /v1/kyb/sessions/{id}/documents
Content-Type: multipart/form-data

doc_type=registration_extract
file=@extract.pdf

# then, when the representative is done:
POST /v1/kyb/sessions/{id}/submit

Lifecycle and scoring

StatusMeaning
createdSession created. No documents uploaded yet.
documents_submittedA registration document was read and the company identified.
approvedAutomated decision: approved. Webhooks fired.
rejectedAutomated decision: rejected, with a plain language reason.
expiredThe 7 day window elapsed without submission.

The company score blends document quality (55%) and the match against your declared data (45%). A file is approved from 50 and above. Three rules reject immediately regardless of the score:

  • The register reports the company as dissolved or suspended.
  • The legal name or registration number does not match what you supplied.
  • No registration document could be read.

KYB webhooks

KYB emits its own event family. Subscribe to them like any other event.

  • kyb.created
  • kyb.documents_submitted
  • kyb.approved
  • kyb.verified
  • kyb.rejected
  • kyb.expired
json
{
  "event": "kyb.verified",
  "timestamp": "2026-08-05T10:14:02Z",
  "session": {
    "id": "kyb_abc123",
    "external_id": "company_42",
    "status": "approved",
    "company": {
      "legal_name": "ACME TRADING SARL",
      "trading_name": "Acme",
      "registration_number": "CI-ABJ-2019-B-12345",
      "tax_number": "1943827K",
      "incorporation_date": "2019-06-14",
      "company_type": "SARL",
      "country": "CIV",
      "registered_address": "Rue du Commerce, Plateau, Abidjan",
      "company_status": "active",
      "directors": [{ "name": "Jean Baptiste Kouame", "role": "Gerant" }]
    },
    "overall_score": 84,
    "risk_level": "low",
    "scores": { "document": 88, "data_match": 100 },
    "representative_session_id": "abc123",
    "ai_extraction": { }
  }
}

The redirect back to your app carries kyb_session_id, kyb_status and kyb_external_id, following the same rules as the KYC callback: treat them as a UX hint and confirm server side.

Next.js (App Router)

Server action creates the session. Client component opens the widget.

ts
// app/actions/verify.ts
"use server";
export async function createVerificationSession(externalId: string) {
  const res = await fetch("https://kalutakyc.com/v1/sessions", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.KALUTA_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      external_id: externalId,
      redirect_url: `${process.env.NEXT_PUBLIC_APP_URL}/kyc/done`,
    }),
    cache: "no-store",
  });
  if (!res.ok) throw new Error("Failed to create session");
  return res.json() as Promise<{ session_id: string; verification_url: string }>;
}
tsx
// app/kyc/page.tsx
"use client";
import Script from "next/script";
import { useState } from "react";
import { createVerificationSession } from "@/app/actions/verify";
declare global { interface Window { KalutaKYC: any } }

export default function KycPage() {
  const [status, setStatus] = useState("");
  const start = async () => {
    const { verification_url } = await createVerificationSession("user_42");
    window.KalutaKYC.open({
      url: verification_url,
      onComplete: (r: any) => setStatus(`${r.status} (score ${r.score})`),
    });
  };
  return (<>
    <Script src="https://kalutakyc.com/embed.js" strategy="afterInteractive" />
    <button onClick={start} className="bg-blue-600 text-white px-5 py-3 rounded-lg">
      Verify my identity
    </button>
    <p>{status}</p>
  </>);
}

PHP and HTML

php
<?php // create-session.php
header("Content-Type: application/json");
$ch = curl_init("https://kalutakyc.com/v1/sessions");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST           => true,
  CURLOPT_HTTPHEADER     => [
    "X-API-Key: " . getenv("KALUTA_API_KEY"),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "external_id"  => $_POST["user_id"] ?? "anon",
    "redirect_url" => "https://example.com/kyc/done",
  ]),
]);
$resp = curl_exec($ch);
http_response_code(curl_getinfo($ch, CURLINFO_HTTP_CODE));
echo $resp;
html
<!-- verify.html -->
<button id="go">Verify my identity</button>
<script src="https://kalutakyc.com/embed.js"></script>
<script>
  document.getElementById("go").addEventListener("click", async () => {
    const r = await fetch("/create-session.php", { method: "POST" });
    const { verification_url } = await r.json();
    KalutaKYC.open({
      url: verification_url,
      onComplete: (result) => alert("Status: " + result.status),
    });
  });
</script>

Node.js Express

javascript
import express from "express";
import crypto from "node:crypto";
const app = express();

app.post("/api/create-session", express.json(), async (req, res) => {
  const r = await fetch("https://kalutakyc.com/v1/sessions", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.KALUTA_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ external_id: req.body.userId }),
  });
  res.status(r.status).json(await r.json());
});

app.post("/hooks/kaluta", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body;
  const sig = req.get("x-kaluta-signature") || "";
  const [ts, v1] = sig.split(",").map(p => p.split("=")[1]);
  const expected = crypto
    .createHmac("sha256", process.env.KALUTA_WEBHOOK_SECRET)
    .update(`${ts}.${raw}`).digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
    return res.sendStatus(401);
  }
  const { event } = JSON.parse(raw.toString());
  if (event === "session.approved") { /* unlock account */ }
  res.sendStatus(200);
});

Python FastAPI

python
import os, hmac, hashlib, httpx
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
API_KEY = os.environ["KALUTA_API_KEY"]
SECRET  = os.environ["KALUTA_WEBHOOK_SECRET"].encode()

@app.post("/api/create-session")
async def create_session(payload: dict):
    async with httpx.AsyncClient() as client:
        r = await client.post(
            "https://kalutakyc.com/v1/sessions",
            headers={"X-API-Key": API_KEY},
            json={"external_id": payload["user_id"]},
        )
    return r.json()

@app.post("/hooks/kaluta")
async def webhook(request: Request):
    raw = await request.body()
    sig = request.headers.get("x-kaluta-signature", "")
    ts, v1 = (p.split("=")[1] for p in sig.split(","))
    expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(v1, expected):
        raise HTTPException(status_code=401)
    return {"ok": True}

Python Django

python
# views.py
import os, json, hmac, hashlib, requests
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

API_KEY = os.environ["KALUTA_API_KEY"]
SECRET  = os.environ["KALUTA_WEBHOOK_SECRET"].encode()

@require_POST
def create_session(request):
    body = json.loads(request.body)
    r = requests.post(
        "https://kalutakyc.com/v1/sessions",
        headers={"X-API-Key": API_KEY},
        json={"external_id": body["user_id"]},
        timeout=10,
    )
    return JsonResponse(r.json(), status=r.status_code)

@csrf_exempt
@require_POST
def kaluta_webhook(request):
    raw = request.body
    sig = request.headers.get("X-Kaluta-Signature", "")
    ts, v1 = (p.split("=")[1] for p in sig.split(","))
    expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(v1, expected):
        return HttpResponse(status=401)
    return HttpResponse("ok")

PHP Laravel

php
// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\{Http, Route};

Route::post("/create-session", function (Request $r) {
    $resp = Http::withHeaders(["X-API-Key" => config("services.kaluta.key")])
        ->post("https://kalutakyc.com/v1/sessions", [
            "external_id" => $r->input("user_id"),
        ]);
    return response()->json($resp->json(), $resp->status());
});

Route::post("/hooks/kaluta", function (Request $r) {
    $raw = $r->getContent();
    $sig = $r->header("X-Kaluta-Signature", "");
    [$ts, $v1] = array_map(fn($p) => explode("=", $p)[1], explode(",", $sig));
    $expected = hash_hmac("sha256", "$ts.$raw", config("services.kaluta.webhook_secret"));
    abort_unless(hash_equals($expected, $v1), 401);
    return response("ok");
})->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class]);

Plain HTML

Never call POST /v1/sessions from the browser

That would expose your secret API key to every visitor. Always proxy through a backend.
html
<button id="go">Verify my identity</button>
<p id="result"></p>
<script src="https://kalutakyc.com/embed.js"></script>
<script>
  document.getElementById("go").onclick = async () => {
    const res = await fetch("/api/create-session", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ user_id: "user_42" }),
    });
    const { verification_url } = await res.json();
    KalutaKYC.open({
      url: verification_url,
      onComplete: (r) => {
        document.getElementById("result").textContent =
          "Final status: " + r.status + " (score " + r.score + ")";
      },
    });
  };
</script>

Webhooks

Configure endpoints in Dashboard → Webhooks. Every event is signed with HMAC SHA256 in theX-Kaluta-Signature header. Always verify the signature before trusting the payload.

Signature format

The header uses a Stripe like scheme so receivers can detect replays.

http
X-Kaluta-Signature: t=1722070800,v1=8f4b3a...

To verify: recompute HMAC over t.raw_body and compare with v1. Reject if t is older than 5 minutes.

Events

  • session.created
  • session.document_submitted
  • session.face_submitted
  • session.approved
  • session.verified
  • session.rejected
  • session.expired

session.verified is an alias of session.approved — it fires on every successful identity confirmation, whether automatic or triggered by our operations team. Subscribe to it if you only care about "this person is who they claim to be".

Payload

Terminal events carry the full result — extracted identity, every sub-score, and the raw AI extraction — so your server can update its records from the webhook alone, without a follow-up API call:

json
{
  "event": "session.verified",
  "timestamp": "2026-08-03T14:23:11Z",
  "session": {
    "id": "abc123",
    "external_id": "user_42",
    "status": "approved",
    "identity": {
      "first_name": "Jean Baptiste",
      "last_name": "Kouame",
      "date_of_birth": "1992-03-15",
      "gender": "M",
      "nationality": "CIV",
      "document_type": "national_id",
      "document_number": "CI004587219",
      "document_expiry": "2030-08-22"
    },
    "overall_score": 87,
    "risk_level": "low",
    "rejection_reason": null,
    "scores": { "document": 92, "face": 88, "liveness": null, "poa": null },
    "checks_required": { "liveness": false, "proof_of_address": false },
    "ai_extraction": { /* raw model output: MRZ, confidence, tampering_signs, ... */ },
    "proof_of_address": null,
    "attempt_count": 1,
    "created_at": "2026-08-03T14:20:05Z",
    "completed_at": "2026-08-03T14:23:11Z"
  }
}

Idempotency

We retry failed deliveries with exponential backoff. Return a 2xx within 10 seconds and make your handler idempotent.

Result schema

A completed session looks like this.

json
{
  "id":            "abc123",
  "external_id":   "user_42",
  "status":        "approved",
  "document_type": "passport",
  "first_name":    "Ada",
  "last_name":     "Lovelace",
  "date_of_birth": "1815-12-10",
  "nationality":   "GBR",
  "document_number":  "P12345678",
  "document_expiry":  "2030-01-01",
  "document_score":   92,
  "face_score":       96,
  "liveness_score":   90,
  "overall_score":    93,
  "risk_level":       "low",
  "extracted_data":   { /* full OCR/MRZ payload */ },
  "checks":           { "mrz_detected": true, "face_match": true, "liveness": true },
  "metadata":         { "plan": "premium" },
  "created_at":       "2026-06-01T12:00:00Z",
  "completed_at":     "2026-06-01T12:01:42Z"
}

Scoring

Overall score is a weighted blend. Weights adapt to which steps your business enabled:

  • Document — image quality plus AI extraction confidence and tampering analysis.
  • Face match — selfie vs document photo, computed by a deep face recognition model.
  • Liveness — anti-spoofing, only when enabled in your settings.
  • Proof of address — document quality, holder name match, and recency, only when enabled.

Base weights are document 55% + face 45%; enabling liveness shifts to 40/35/25, and proof of address reserves a further 20%.

Decision: 60 and up = approved · below 60 = rejected. Independently of the score, a failed face match, an identity mismatch against the values you supplied, or a failed liveness check rejects immediately.

Errors

The API returns standard HTTP status codes with a JSON body.

json
{ "detail": "Invalid API key" }
StatusMeaning
400Bad request. Required field missing or invalid.
401Invalid API key or expired JWT.
402Insufficient balance. Top up to continue.
403Action not allowed on this resource.
404Resource not found.
409Conflict, for example session already approved or rejected.
410Session expired.
413Uploaded file is too large (max 20 MB).
429Rate limited. Retry after a short backoff.
500Server error. Retry. Contact support if persistent.

Testing

Two easy ways to try the flow without writing code.

  • The embed demo page lets you paste any verification_url and open the inline widget with live event logs.
  • Replay a webhook delivery from Dashboard → Webhooks → Endpoint → Deliveries.

New accounts receive 5 free verifications so you can run a real end to end test without committing to billing.

API reference

Every endpoint at a glance.

MethodEndpointDescription
POST/v1/sessionsCreate a verification session
GET/v1/sessionsList your sessions
GET/v1/sessions/{id}Retrieve a session with results
POST/v1/sessions/{id}/document-videoUpload the tilt capture for the security check
POST/v1/sessions/{id}/retrySpawn a fresh session after a rejection
GET/v1/sessions/{id}/publicPublic read
POST/v1/kyb/sessionsCreate a KYB (company) session
GET/v1/kyb/sessionsList your KYB sessions
GET/v1/kyb/sessions/{id}Retrieve a KYB session
POST/v1/kyb/sessions/{id}/documentsUpload a company document
POST/v1/kyb/sessions/{id}/submitSubmit the company file for a decision
PUT/v1/auth/meUpdate profile and verification step toggles
GET/v1/api-keysList API keys
POST/v1/api-keysCreate a new API key
DELETE/v1/api-keys/{id}Revoke a key
GET/v1/webhooksList webhook endpoints
POST/v1/webhooksCreate a webhook endpoint
DELETE/v1/webhooks/{id}Delete a webhook endpoint
GET/v1/billing/summaryAccount balance and usage
POST/v1/billing/topupAdd credits to your balance

Need a hand?

Email support@kalutakyc.com for integration questions, security disclosures, or a DPA request.