Civic Anchor
Institutions · Integration guide · South Africa

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).

Contextbiometricinstitutionalcredentialp2ptemporal

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

1

Verifier initiates

Verifier calls p2p.initiateChallenge with the verifiee's userId and their current GPS coordinates. Platform checks velocity caps before issuing an OTP.

2

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.

3

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.

4

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.

MultiplierPoints (each party)
×880
×660
×440
×110

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.

ClassWeight
telco1.00
bank1.00
government1.00
verifying_authority0.90
refugee_office0.95
border_post0.90

Tier 2b — Employer class

Captive workforce with employment records. Fast onboarding. Weight: 0.60–0.85.

ClassWeight
mining_employer0.85
corporate_employer0.80
employer0.80
agri_employer0.70
educational0.65
gig_platform0.60

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.

ClassWeight
retailer0.70
postal_service0.75
social_grant_office0.80
health_facility0.75
ngo0.65
union0.55
religious_org0.50
informal_retail0.35
stokvel0.30

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 classesDiversity multiplier
1×0.50
2×0.75
3×0.90
4+×1.00

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.

IdentifierMutabilityUse 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.