How the verification API works, end to end.
Every value in this guide — endpoint paths, weight tables, velocity caps, multipliers — is taken directly from the live codebase. Nothing is estimated or aspirational.
Challenge-based flow
How a verification ping moves through the stack
The flow is challenge-based, not monolithic. Your system creates a challenge; the subject approves it from their device; the platform resolves and returns a signed result. User consent is cryptographically enforced — the platform cannot respond without an active subject approval.
01
Your system calls
POST /api/v1/challenges
Send { did, transactionRef, purpose } in the JSON body. Authenticate with your Bearer API key (scope: challenge:create) over mTLS. Returns { challengeId, expiresAt } with a 5-minute window.
02
Subject receives notification
Push notification arrives in their PWA
The platform dispatches a push to the subject's enrolled device(s). The challenge window is 5 minutes (CHALLENGE_TTL_MS). The subject reviews the requesting institution's name, purpose, and amount before approving.
03
Platform resolves
Aggregates RICA telco + FICA bank + P2P anchors
On subject approval, the platform assembles the five-pillar score from the subject's current anchor state. An optional context parameter re-weights the pillars for your use case.
04
HSM-signed response
Signed verification response + webhook event fired
Response contains: score, breakdown, credential list, DID, continuity hash, and a platform signature. The webhook event fires synchronously before the HTTP response returns.
05
Subject earns credit
5–10% of the verification fee rebated as Civic Credit
The subject's Civic Credit ledger is credited atomically with the verification. Credits accumulate and can offset future verification fees.
Request shape
POST /api/v1/challenges
Authorization: Bearer <api_key>
Content-Type: application/json
{
"did": "did:civicanchor:018e4f2c-7a91-7b3d-9aae-3c41e9a7f1d2",
"transactionRef": "INV-2026-00042", // your reference, min 1 char
"purpose": "Employment background check", // max 200 chars
"amountCents": 0, // optional — shown in push
"currency": "ZAR" // optional, defaults ZAR
}
→ 201 Created
{
"challengeId": "<uuid>",
"expiresAt": "<iso8601>" // 5 minutes from creation
}Poll for outcome
GET /api/v1/challenges/:challengeId
Authorization: Bearer <api_key>
→ 200 OK
{
"challengeId": "<uuid>",
"status": "pending" | "approved" | "denied" | "expired",
"transactionRef": "INV-2026-00042",
"purpose": "Employment background check",
"amountCents": 0,
"currency": "ZAR",
"expiresAt": "<iso8601>",
"respondedAt": "<iso8601> | null",
"responseSignature": "<hmac-sha256> | null" // set on approved
}Full OpenAPI specification at /api/docs. Sandbox credentials via integrations@civicanchor.id.
Re-weighted for your use case
Contextual score profiles
Pass an optional context parameter when creating a challenge. The platform re-weights the five pillar scores for that use case and returns both the raw score and the contextual score. No new data is collected — the same five pillars are simply weighted differently.
P2P attestations are set to 0 for contexts where peer-attestation is not a decision signal (border crossings, government clearance, diplomatic protocol).
| Context | biometric | institutional | credential | p2p | temporal |
|---|---|---|---|---|---|
standard General-purpose identity verification. | 30% | 25% | 10% | 30% | 5% |
credit_loan Bank credit, microfinance, or lending decision. | 25% | 35% | 30% | 5% | 5% |
employment_check Pre-employment screening or background check. | 20% | 30% | 35% | 10% | 5% |
border_crossing Immigration, customs, or cross-border movement. | 40% | 30% | 25% | 0% | 5% |
housing_application Rental application, tenant screening. | 20% | 25% | 40% | 10% | 5% |
government_clearance Public sector access, civic grant eligibility. | 35% | 35% | 25% | 0% | 5% |
diplomatic_protocol DIRCO accreditation, diplomatic / consular access. | 35% | 40% | 25% | 0% | 0% |
All rows sum to 100%. The contextual score is always in [0, 1 000] — the same range as the raw Civic Trust Score.
Peer verification
P2P attestation — how it works
P2P handshakes let two enrolled users attest to each other after a real-world interaction. Both parties earn the same score. The system enforces four anti-farming pillars to prevent manufactured attestations.
OTP flow
Verifier initiates
Verifier calls p2p.initiateChallenge with the verifiee's userId and their current GPS coordinates. Platform checks velocity caps before issuing an OTP.
OTP dispatched
A 6-digit OTP is generated from OS CSPRNG and dispatched to the verifier's RICA-bound MSISDN. Valid for 90 seconds. The verifier reads it aloud or shows their screen to the verifiee.
Verifier finalises
Verifier calls p2p.finalizeHandshake with the challengeId and OTP. Platform validates OTP hash, re-checks velocity, checks GPS movement plausibility (≤120 km/h between the verifier's samples), then writes the handshake row.
Points awarded
Both verifier and verifiee receive scoreAwarded = 10 × anchor class multiplier. Score recomputation runs asynchronously. A server-issued SHA-256 consent receipt is stored on the handshake row (v1; full WebAuthn assertion ships M3.5).
Anchor class multipliers
scoreAwarded = 10 × multiplier. Both parties earn this value. Multiplier is determined by the verifiee's best institutional anchor class.
| Multiplier | Points (each party) | Verifiee anchor class | Rationale |
|---|---|---|---|
| ×8 | 80 | bank, government, verifying_authority | Tier 1 regulatory anchors. FICA KYC already performed by these institutions. |
| ×6 | 60 | telco | RICA-bound SIM registration. Strong identity signal; lower than bank. |
| ×4 | 40 | employer, corporate_employer, mining_employer, agri_employer, gig_platform, educational, border_post, refugee_office | Captive workforce anchors. Employment records are a solid identity signal. |
| ×1 | 10 | All other anchor classes, or no anchor | Default multiplier when the verifiee has no institutional anchor in a higher tier. |
Velocity caps (anti-farming)
5 handshakes per hour
Per verifier. Resets on the rolling hour window.
20 handshakes per 24 hours
Per verifier. Rolling 24-hour window.
3 handshakes per pair per 30 days
Same verifier ↔ verifiee pair. Prevents mutual farming.
10 inbound handshakes per 24 hours
Per verifiee. Limits inbound farming attacks.
Geo-implausibility alert: if two GPS samples from the same verifier are separated by more than 120 km/h implied ground speed, the handshake is blocked and a severity-9 alert is raised on that account. This detects device cloning and GPS spoofing.
Institutional pillar
Institution trust weights
Each institution class has a default trust weight in [0.00, 1.00]. These weights feed the institutional pillar score calculation. Per-instance overrides are possible under the partner agreement. The weights below are the system defaults.
Tier 1 — Regulatory anchor
Regulator-bound institutions. Low fraud risk, high onboarding cost. Weight: 1.00.
| Class | Weight | Examples (ZA) |
|---|---|---|
| telco | 1.00 | MTN, Vodacom, Telkom, Cell C |
| bank | 1.00 | ABSA, FNB, Standard Bank, Nedbank, Capitec |
| government | 1.00 | DHA, SASSA, Home Affairs branches |
| verifying_authority | 0.90 | MIE, LexisNexis, iFacts, AfriGIS |
| refugee_office | 0.95 | UNHCR-accredited processing centres |
| border_post | 0.90 | BEITBRIDGE, Lebombo, Kazungula |
Tier 2b — Employer class
Captive workforce with employment records. Fast onboarding. Weight: 0.60–0.85.
| Class | Weight | Examples (ZA) |
|---|---|---|
| mining_employer | 0.85 | Anglo American, Sibanye-Stillwater |
| corporate_employer | 0.80 | Registered companies, large retailers |
| employer | 0.80 | Formal employer — general class |
| agri_employer | 0.70 | Commercial farms, agri-processing |
| educational | 0.65 | Universities, TVET colleges |
| gig_platform | 0.60 | Uber, Bolt Food, SweepSouth |
Tier 2 — Reach
Already in front of underserved populations. Two-person rule applies to informal_retail, religious_org, stokvel (NOD-3). Weight: 0.30–0.75.
| Class | Weight | Examples (ZA) |
|---|---|---|
| retailer | 0.70 | Checkers, Pick n Pay, Shoprite |
| postal_service | 0.75 | South African Post Office |
| social_grant_office | 0.80 | SASSA pay-points |
| health_facility | 0.75 | Public clinics, CHCs |
| ngo | 0.65 | Registered NPOs — high social trust |
| union | 0.55 | COSATU-affiliated unions |
| religious_org | 0.50 | Church, mosque, synagogue, temple |
| informal_retail | 0.35 | Spaza shops, street vendors |
| stokvel | 0.30 | Registered and informal stokvels |
NOD-6 diversity rule
The diversity multiplier
The institutional pillar score is not just the sum of trust weights — it is the sum multiplied by a diversity multiplier that rewards breadth across institution types. A user with four different institution classes (e.g. telco + bank + employer + NGO) scores higher than a user with four anchors from the same class.
| Distinct institution classes | Diversity multiplier | Notes |
|---|---|---|
| 1 | ×0.50 | Single-source — high collusion risk |
| 2 | ×0.75 | — |
| 3 | ×0.90 | — |
| 4+ | ×1.00 | Cap — diminishing returns beyond 4 classes |
Formula: institutionalScore = Σ(trustWeight) × diversityMultiplier × 60, clamped to 250. The ×60 scaling factor maps a "well-established" user with 4 diverse Tier-1/2 anchors (~3.5–4.0 raw) into the 210–240 range — filling the pillar without hitting the cap too easily. platform_admin is excluded from the distinct-class count.
Database key guidance
Which identifier do you store?
Every Civic Anchor identity carries three identifiers. Only one is safe as a foreign key in your CRM.
| Identifier | Mutability | Use as foreign key? |
|---|---|---|
DID did:civicanchor:018e4f2c-… | Immutable. Minted at first enrollment, signed by the partner node, persists for life. | Yes — canonical |
Username @thandi-plumber | Mutable in principle. Validated against reserved names and ASCII confusable squats at claim time. | Display label only |
Continuity hash HMAC-SHA256 of biometric template | Changes on re-enrollment. Proof of biometric continuity, not a persistent identifier. | No — proof, not ID |
Recommended pattern: store the DID in your CRM, display the username for human readability, re-verify the DID on every transaction. The cryptographic chain disambiguates name changes and lookalike username squats.
Async events
Webhook contract
Webhook events are signed with HMAC-SHA256 using your webhook secret. Verify the X-CivicAnchor-Signature header before processing. Events fire synchronously before the HTTP challenge response returns.
verification.completed
Fired when a subject approves a challenge and the platform returns a signed response. Payload: DID, score, breakdown, credential list, contextual scores (if context was specified), timestamp.
verification.duress_flagged
Fired when a subject indicates duress during approval. Payload: DID, challengeId, timestamp. Your system should treat this as a failed verification and not proceed with the transaction.
credential.refresh_requested
Fired when a credential in the subject's profile reaches its refresh window and needs re-issuance. Payload: DID, credentialType, expiresAt. Relevant if you are a credential issuer.
Signature verification (pseudocode)
const sig = req.headers['x-civicanchor-signature']; const expected = hmacSha256(webhookSecret, req.rawBody); if (!timingSafeEqual(sig, expected)) reject(401);
Ready to integrate?
Sandbox API keys, the POPIA Operator agreement, and mTLS certificates are issued at onboarding. No credit card required for sandbox access.