# INFRA API Documentation > INFRA is a digital identity infrastructure platform currently in sandbox/beta mode. Users verify once via KYC, receive signed verification claims, and share them with any integrated app — without re-verification or raw document exposure. All data is for testing purposes only. - API base URL: `https://api.infraidentity.com/api/v1` - OAuth authorization server: `https://auth.infraidentity.com` - Integration support: engineering@infraidentity.com - Human-readable docs: https://docs.infraidentity.com/ ## Contents 1. [Overview](https://docs.infraidentity.com/) 2. [Quick Start](https://docs.infraidentity.com/quickstart/) 3. [Authentication (OAuth 2.0 + PKCE)](https://docs.infraidentity.com/authentication/) 4. [API Reference](https://docs.infraidentity.com/api-reference/) 5. [Webhooks](https://docs.infraidentity.com/webhooks/) 6. [INFRA Connect](https://docs.infraidentity.com/infra-connect/) 7. [Verified Claims](https://docs.infraidentity.com/claims/) 8. [Verifying Credentials](https://docs.infraidentity.com/credentials/) 9. [Scopes & Trust Levels](https://docs.infraidentity.com/scopes/) 10. [Developer Policy](https://docs.infraidentity.com/developer-policy/) --- ## Overview **What is INFRA?** INFRA is a digital identity infrastructure platform currently in sandbox/beta mode for testing. Users verify once via KYC, receive signed verification claims, and share them with any integrated app — without re-verification or raw document exposure. All data is for development and testing purposes only. ### Key properties - **Verify Once, Use Everywhere** — One KYC verification on the INFRA mobile app unlocks access to all integrated applications. - **Verified Claims** — Apps receive INFRA-signed facts (age, residency, KYC status) — never raw documents. - **Standard OAuth 2.0** — Full OAuth 2.0 + PKCE authorization flow. Works with any standard OAuth client library. - **User-Controlled Consent** — Users approve and revoke app access at any time from the INFRA mobile app. **Base URL:** `https://api.infraidentity.com/api/v1` --- ## Quick Start Follow these steps to integrate INFRA into your application. You'll need a registered developer account and an OAuth app. 1. **Register on the Developer Portal** — Visit the INFRA developer portal and create your account. Verify your email to activate access. 2. **Create an OAuth App** — In the portal, create a new application. You'll receive a client_id and client_secret. Set your redirect_uri(s). 3. **Implement the OAuth Flow** — Redirect users to the INFRA authorization endpoint with PKCE. Handle the callback and exchange the code for tokens. 4. **Call the User Data API** — Use the access token to call INFRA API endpoints and retrieve verified user data and signed claims. ### Step 1: Redirect user to INFRA authorization ```javascript // Generate PKCE code verifier and challenge first const authUrl = new URL('https://auth.infraidentity.com/oauth2/auth'); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID'); authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback'); authUrl.searchParams.set('scope', 'kyc:read profile:read claims:read'); authUrl.searchParams.set('state', generateState()); authUrl.searchParams.set('code_challenge', codeChallenge); authUrl.searchParams.set('code_challenge_method', 'S256'); window.location.href = authUrl.toString(); ``` ### Step 2: Exchange code for tokens (backend) ```javascript const response = await fetch('https://auth.infraidentity.com/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authorizationCode, redirect_uri: 'https://yourapp.com/callback', code_verifier: codeVerifier, client_id: 'YOUR_CLIENT_ID', client_secret: 'YOUR_CLIENT_SECRET', }), }); const { access_token, refresh_token, expires_in } = await response.json(); ``` ### Step 3: Fetch verified user data ```javascript // Get user profile const profile = await fetch( 'https://api.infraidentity.com/api/v1/users/{userId}/profile', { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const data = await profile.json(); // { success: true, data: { id, email, first_name, kyc_level, ... } } ``` --- ## Authentication (OAuth 2.0 + PKCE) INFRA uses standard OAuth 2.0 with PKCE (Proof Key for Code Exchange) for all authorization flows. No proprietary SDK required. ### PKCE flow 1. **Generate PKCE Pair** — Create a cryptographically random code_verifier (43-128 chars). Derive code_challenge = BASE64URL(SHA256(code_verifier)). 2. **Authorization Request** — Redirect user to https://auth.infraidentity.com/oauth2/auth with response_type=code, scopes, state, and code_challenge. 3. **User Consent** — INFRA shows the user a consent screen listing the requested scopes. User approves or denies. 4. **Authorization Code** — INFRA redirects to your redirect_uri with ?code=AUTH_CODE&state=YOUR_STATE. 5. **Token Exchange** — Your backend POSTs to /oauth2/token with the code, code_verifier, client credentials. Receives access_token + refresh_token. 6. **API Calls** — Use the access_token as a Bearer token in the Authorization header for all INFRA API requests. ### Scopes | Scope | Description | Trust level | |---|---|---| | `profile:read` | Name, email, country, profile photo | Standard | | `kyc:read` | KYC level, status, verification date | Standard | | `claims:read` | Signed verification claims (age, residency, etc.) | Standard | | `documents:metadata` | Document type and verification status (no content) | Standard | | `documents:read` | JWE-encrypted sensitive documents | Trusted | ### Token lifetimes | Token | Lifetime | Notes | |---|---|---| | Access Token | 1 hour | Use for API calls. Refresh when expired. | | Refresh Token | 30 days | Exchange for new access token. Rotates on use. | --- ## API Reference All endpoints require a valid Bearer access token obtained via the OAuth 2.0 flow. **Base URL:** `https://api.infraidentity.com/api/v1` ### GET /users/:userId/profile Returns the verified profile for a user. Requires `profile:read` scope. ```javascript const response = await fetch( `https://api.infraidentity.com/api/v1/users/${userId}/profile`, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); // Response { "success": true, "data": { "id": "uuid", "email": "user@example.com", "first_name": "John", "last_name": "Doe", "country": "NG", "kyc_level": "level_1", "risk_score": 75 } } ``` ### GET /users/:userId/kyc Returns KYC verification status. Requires `kyc:read` scope. ```javascript // Response { "success": true, "data": { "kyc_level": "level_1", "status": "verified", "verified_at": "2026-01-15T10:30:00Z", "provider": "persona" } } ``` ### GET /users/:userId/claims Returns signed verification claims for a user. Requires `claims:read` scope. ```javascript // Response { "success": true, "data": { "claims": [ { "claim_type": "kyc_verified", "provider": "persona", "revoked": false }, { "claim_type": "age_over_18", "provider": "infra", "revoked": false }, { "claim_type": "country_resident", "provider": "infra", "revoked": false }, { "claim_type": "unique_human", "provider": "persona", "revoked": false } ] } } ``` --- ## Webhooks INFRA sends webhook events to your registered endpoint when key actions occur. Configure your webhook URL in the developer portal. ### Event types | Event | Description | |---|---| | `kyc.verified` | User completed KYC successfully | | `kyc.rejected` | KYC verification was rejected | | `consent.granted` | User approved your app's access request | | `consent.revoked` | User revoked your app's access | | `claim.issued` | A new verification claim was issued to a user | | `claim.revoked` | A verification claim was revoked | | `data.deletion_requested` | User requested data deletion (GDPR) | ### Webhook payload ```javascript // Example: kyc.verified event { "event_type": "kyc.verified", "event_id": "evt_01HXYZ...", "timestamp": "2026-01-15T10:30:00Z", "data": { "user_id": "usr_01HXYZ...", "kyc_level": "level_1", "status": "verified", "provider": "persona" } } ``` ### Verifying signatures Every webhook request includes an `x-webhook-signature` header. Always verify this before processing the event. ```javascript const crypto = require('crypto'); function verifyWebhook(req, webhookSecret) { const signature = req.headers['x-webhook-signature']; const expectedSig = crypto .createHmac('sha256', webhookSecret) .update(JSON.stringify(req.body)) .digest('hex'); if (signature !== expectedSig) { throw new Error('Invalid webhook signature'); } return true; } // In your Express route: app.post('/webhooks/infra', (req, res) => { verifyWebhook(req, process.env.INFRA_WEBHOOK_SECRET); const { event_type, data } = req.body; switch (event_type) { case 'kyc.verified': // Update user KYC status in your DB break; case 'consent.revoked': // Remove user access tokens break; } res.json({ received: true }); }); ``` --- ## INFRA Connect INFRA Connect lets you request user consent directly via API — no OAuth redirect required. The user receives an in-app notification to approve or deny. ### How it works 1. **Request Consent** — POST to /connect/request with the user's INFRA ID and the scopes you need. 2. **User Gets Notified** — The user receives a push notification in the INFRA mobile app with your request details. 3. **User Approves or Denies** — User reviews and approves or denies the request in-app. 4. **Poll for Result** — Poll GET /connect/result/:requestId or receive a consent.granted webhook. ### Request consent ```javascript const response = await fetch( 'https://api.infraidentity.com/api/v1/connect/request', { method: 'POST', headers: { 'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ infra_user_id: 'usr_01HXYZ...', scopes: ['profile:basic', 'kyc:status'], callback_url: 'https://yourapp.com/webhooks/infra', }), } ); // Response { "request_id": "req_01HXYZ...", "status": "pending", "expires_in": 300 } ``` ### Poll for the result ```javascript const result = await fetch( `https://api.infraidentity.com/api/v1/connect/result/${requestId}`, { headers: { 'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`, }, } ); // Response when approved { "request_id": "req_01HXYZ...", "status": "approved", "user_id": "usr_01HXYZ...", "granted_scopes": ["profile:basic", "kyc:status"], "access_token": "eyJ..." } ``` --- ## Verified Claims INFRA issues claims derived from verified identity data and signs them. Apps receive a verified fact — never the underlying data. ### Available claims #### `kyc_verified` — KYC Verified - **What it means:** User has passed KYC verification via an accredited KYC vendor. - **What it proves:** Identity has been verified by an accredited KYC provider. - **Not shared:** No personal data, documents, or scores are shared. #### `age_over_18` — Age Over 18 - **What it means:** User is 18 years of age or older. - **What it proves:** Age threshold met — derived from verified date of birth. - **Not shared:** Actual date of birth is never shared with the app. #### `country_resident` — Country Resident - **What it means:** User's verified country of residence. - **What it proves:** Residency in a specific country, confirmed during KYC. - **Not shared:** Full address and document details are not shared. #### `unique_human` — Unique Human - **What it means:** User has passed duplicate identity detection. - **What it proves:** This is a real, unique person — not a duplicate account. - **Not shared:** Biometric data is processed by the KYC vendor and never stored by INFRA. ### Claim Revocation Claims can be revoked by the user at any time from the INFRA mobile app, or automatically if the underlying KYC verification is invalidated. Your app will receive a `claim.revoked` webhook event. --- ## Verifying Credentials Alongside the claims list, GET /users/{userId}/claims returns credential: the same claims as an SD-JWT signed by INFRA with ES256. Verify it in your own backend with INFRA's public keys, without calling INFRA on every check. 1. **Read the user's claims** — Call GET /api/v1/users/{userId}/claims with a claims:read token. The response includes credential and credential_format: "dc+sd-jwt". 2. **Fetch INFRA's public keys** — Load https://api.infraidentity.com/.well-known/jwks.json and cache it (up to 5 minutes). Pick the key whose kid matches the credential header. 3. **Verify the signature and checks** — Check the ES256 signature, issuer, expiry, that aud is your own client_id, and that environment matches your mode (live or test). 4. **Read the disclosed claims** — Each claim is a disclosure: hash it with SHA-256 and confirm the digest is listed in the signed _sd array before trusting it. ### Response ```json { "success": true, "data": { "user_id": "a3f1…", "claims": [ { "claim_type": "kyc_verified", "provider": "persona", "proof_hash": "…", "expires_at": null } ], "credential": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImRjK3NkLWp3dCIsImtpZCI6Ii4uLiJ9.eyJ…~WyJzYWx0Ii…~", "credential_format": "dc+sd-jwt" } } ``` ### Verify in Node.js ```javascript import { createRemoteJWKSet, jwtVerify } from 'jose'; import { createHash } from 'node:crypto'; const JWKS = createRemoteJWKSet( new URL('https://api.infraidentity.com/.well-known/jwks.json') ); export async function verifyInfraCredential(credential, { clientId, mode = 'live' }) { const [jwt, ...rest] = credential.split('~'); const disclosures = rest.slice(0, -1); // the credential ends with "~" const { payload } = await jwtVerify(jwt, JWKS, { algorithms: ['ES256'], issuer: 'https://api.infraidentity.com', audience: clientId, // must be YOUR client_id typ: 'dc+sd-jwt', }); if (payload.environment !== mode) { throw new Error(`Credential is for ${payload.environment}, not ${mode}`); } const signed = new Set(payload._sd); const claims = {}; for (const d of disclosures) { const digest = createHash('sha256').update(d).digest('base64url'); if (!signed.has(digest)) throw new Error('Disclosure not covered by the signature'); const [, name, value] = JSON.parse(Buffer.from(d, 'base64url').toString('utf8')); claims[name] = value; // e.g. kyc_verified: { verified: true, provider, issued_at, expires_at } } return { userId: payload.sub, claims }; } ``` ### Rules - Always check aud against your own client_id and environment against your mode: a test credential must never pass in live. - Credentials expire after 24 hours; read the claims again for a fresh one. Each claim also carries its own expires_at. - You can forward a subset of disclosures to a partner (selective disclosure); the signature still verifies. - Keys rotate: always select by kid from the JWKS, and refresh the JWKS when you see an unknown kid. - These are signed verification claims, not zero-knowledge proofs. POST /api/v1/claims/verify keeps working if you prefer to ask INFRA. Prefer a library? Any SD-JWT VC implementation works, for example @sd-jwt/sd-jwt-vc from the OpenWallet Foundation, as long as you still check aud and environment. --- ## Scopes & Trust Levels INFRA uses two trust tiers. Standard apps can access public profile data and verified claims. Trusted apps can additionally request encrypted sensitive documents. ### Standard Apps Default tier for all registered developer apps. Access to public identity data and verified claims. - User profile (name, email, country) - KYC level and verification status - Verified claims - Document metadata (type only) - Consent management - Webhook events ### Trusted Apps Elevated tier for regulated platforms (fintech, healthcare, etc.). Requires approval. Includes everything in Standard plus: - JWE-encrypted passport data - National ID number (NIN) - Bank Verification Number (BVN) - Full document images (encrypted) - Enhanced KYC data ### Full scope reference | Scope | Returns | Tier | |---|---|---| | `profile:read` | id, email, first_name, last_name, country, photo_url | Standard | | `kyc:read` | kyc_level, status, verified_at, provider | Standard | | `claims:read` | Array of verification claims with type, provider, revoked status | Standard | | `documents:metadata` | Document type, country, verification status (no content) | Standard | | `documents:read` | JWE-encrypted document content (passport, NIN, BVN) | Trusted | ### JWE Encryption Sensitive documents returned by Trusted apps are encrypted using JSON Web Encryption (JWE). Your app must hold the corresponding private key to decrypt the payload. Key exchange is handled during the Trusted app approval process. --- ## Developer Policy All developers integrating with the INFRA API must read and comply with this policy. Violations may result in immediate suspension of access. > **Important Notice:** By integrating with this API, you agree to comply with all data access, consent, and security requirements defined below. Failure to comply may result in immediate suspension of access, revocation of credentials, and removal from the platform. ### 1. Consent Enforcement Rules Consent is mandatory for all data access. Applications must not access user data without active consent, attempt to bypass consent flows, or cache and reuse expired consent tokens. On consent revocation: - All access must stop immediately - Tokens must be treated as invalid - No further API requests should be made on behalf of the user ### 2. Data Usage Restrictions Applications must only use data for the purpose explicitly approved by the user. Applications must not: - Resell user data - Share data with third parties without consent - Use data for profiling beyond approved scope - Store data longer than necessary ### 3. Data Storage Policy Storage of raw identity documents is strongly restricted. Applications should prefer verification status over raw documents and minimize stored personal data. For sensitive data: - Access requires explicit approval and trusted developer status - Storage must follow secure handling practices ### 4. Revocation Handling (CRITICAL) Upon receiving a `consent_revoked` event, applications must: - Immediately stop processing user data - Disable user-specific features relying on that data - Treat previously obtained data as non-current Applications may retain data only if required for legal or operational purposes and it is no longer actively processed. ### 5. Account Deletion Enforcement Upon receiving a `user_deleted` event, applications must: - Permanently delete all user-related data - Remove all stored identity information - Confirm deletion where applicable **This action is mandatory and non-optional.** ### 6. Token and Security Rules Applications must: - Securely store API keys and tokens - Never expose credentials client-side - Implement server-side verification Applications must not: - Hardcode secrets in public code - Transmit tokens over insecure channels - Reuse expired tokens ### 7. Webhook Compliance Applications must: - Verify webhook signatures - Process webhook events reliably - Handle retries and idempotency Ignoring these events is non-compliance: - `consent_revoked` - `user_deleted` ### 8. Abuse and Misuse The following behaviors are strictly prohibited: - Excessive or automated consent requests - Scraping or bulk data extraction - Attempting to infer data beyond granted scope - Simulating user actions ### 9. Trusted Developer Requirements Access to sensitive claims is restricted. Applications must undergo review before accessing sensitive data, justify data usage, and maintain compliance standards. Approval may be revoked at any time. ### 10. Rate Limiting and Access Control Applications must respect rate limits. Excessive requests may result in: - Temporary throttling - API suspension - Permanent access removal ### 11. Compliance and Enforcement We reserve the right to audit application behavior, monitor usage patterns, and suspend or revoke access without prior notice. ### 12. Data Validity Disclaimer (IMPORTANT) Access to data does not imply permanent validity. Applications must: - Revalidate data where required - Respect expiration timestamps - Not rely on stale or outdated information