LandVerify API Documentation

Securely submit and track land verification requests via our REST API.

Base URL: https://app.landverify.ng

Quick Start

🔑

1. Get API Key

Contact support@landverify.ng

📤

2. Submit Request

POST to /verification/submit

🔄

3. Poll for Results

GET from /findings endpoint

Authentication

Include your API key in the request header for all endpoints:

x-api-key: YOUR_API_KEY

Security Warning: Never expose your API key in public repositories, client-side code, or version control.

File Upload Guide

Files could be Base64 data URIs, public URLs, or other 3rd party file hosts.

File Requirements:

  • Maximum size: 10MB per file
  • Supported formats: PNG, JPG, JPEG, PDF
  • Required for: Full verification (paymentType: full)

Converting Files to Base64

// Convert File to Base64
function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = error => reject(error);
  });
}

// Usage example
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const base64 = await fileToBase64(file);
  console.log(base64); // "data:image/png;base64,iVBORw0KG..."
});

1. Submit a Verification Request

POST /api/verification/submit

Submit a new land verification request.

🟢 Regular Verification

paymentType: regular

Files are optional. Basic address verification without document review.

🔵 Full Verification

paymentType:full

Files are required. Complete verification with document review.

Request Parameters

FieldTypeRequiredDescription
addressstringProperty address to verify
userIdnumberUser ID of the account created via LandVerify
paymentTypestringregular or full
filesstring[]*Array of base64 data URIs (required for full)
paymentAmountnumberAmount in local currency
paymentStatusstringpending or completed
lgastringLocal Government Area
statestringState name
landsizestringSize of property (e.g., 500sqm)
latitudenumberGPS latitude
longitudenumberGPS longitude

Example Request (Full Verification)

curl -X POST "https://app.landverify.ng/api/verification/submit" \
  -H "Content-Type: application/json" \
  -H "x-api-key: YOUR_API_KEY" \
  -d '{
    "address": "123 Main St, Lagos",
    "paymentType": "full",
    "paymentAmount": 15000,
    "paymentStatus": "completed",
    "files": [
      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8Xw8AAoMBg6d9fZcAAAAASUVORK5CYII="
    ],
    "userId": 3386,
    "lga": "Ikeja",
    "state": "Lagos",
    "landsize": "500sqm",
    "latitude": 6.5244,
    "longitude": 3.3792
  }'

Success Response (200)

{
  "status": "SUBMITTED",
  "verificationId": "2e5ab633-ea41-4d99-809c-efbf1e1e6925",
  "partnerId": 3,
  "paymentType": "full",
  "uploadedFiles": [
    "https://res.cloudinary.com/dsni5uhop/image/upload/v17280123/landverify_uploads/doc1.jpg"
  ],
  "failedUploads": []
}

Response Fields:

  • status - Current status: SUBMITTED, IN_PROGRESS, or COMPLETED
  • verificationId - Unique ID for polling findings
  • partnerId - ID of assigned verification partner
  • uploadedFiles - Array of successfully uploaded file URLs
  • failedUploads - Array of failed uploads with reasons

2. Get Verification Findings

GET /api/verification-requests/{id}/findings

Poll this endpoint to check for verification results. Use the verificationId from the submit response.

⚠️ Rate limited: 30 requests per minute per API key

Example Request

curl "https://app.landverify.ng/api/verification-requests/2e5ab633-ea41-4d99-809c-efbf1e1e6925/findings" \
  -H "x-api-key: YOUR_API_KEY"

Response (In Progress)

{
  "findings": null,
  "status": "SUBMITTED",
  "updatedAt": "2025-10-05T10:30:00.000Z"
}

Response (Complete)

{
  "findings": {
    "comments": "Property verified. Clean title with no encumbrances. Suitable for development with standard zoning approvals.",
    "isRegisteredTitledVerified": true,
    "isPropertyFreeOfAcquisition": true,
    "DoesAddressMatchSurvey": true,
    "erosionOrFloodRisk": false,
    "locatedInMixedArea": true,
    "suitableTopography": true
    // ...additional fields
  },
  "status": "COMPLETED",
  "updatedAt": "2025-10-05T12:45:00.000Z"
}

Polling Guidelines:

  • ✓ Poll every 30-60 seconds
  • ✓ Stop when status === COMPLETED and findings !== null
  • ✓ Implement exponential backoff on errors
  • ✓ Set maximum polling duration (e.g., 30 minutes)

Complete Implementation Example

// Complete End-to-End Example
import { useState } from 'react';

function LandVerification() {
  const [verificationId, setVerificationId] = useState(null);
  const [findings, setFindings] = useState(null);
  const [status, setStatus] = useState('idle');
  
  // Step 1: Convert files to base64
  const handleFileUpload = async (files) => {
    const base64Files = await Promise.all(
      Array.from(files).map(file => fileToBase64(file))
    );
    return base64Files;
  };

  // Step 2: Submit verification request
  const submitVerification = async (address, files) => {
    setStatus('submitting');
    
    const base64Files = await handleFileUpload(files);
    
    const response = await fetch('/api/verification/submit', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': 'YOUR_API_KEY'
      },
      body: JSON.stringify({
        address,
        files: base64Files,
        userId: 3386, // Your user's ID
        paymentType: 'full',
        paymentAmount: 15000,
        paymentStatus: 'completed',
        lga: 'Ikeja',
        state: 'Lagos',
        landsize: '500sqm'
      })
    });
    
    const data = await response.json();
    setVerificationId(data.verificationId);
    setStatus('polling');
    
    // Step 3: Start polling for results
    startPolling(data.verificationId);
  };

  // Step 3: Poll for findings
  const startPolling = (id) => {
    const pollInterval = setInterval(async () => {
      const response = await fetch(
        `/api/verification-requests/${id}/findings`,
        { headers: { 'x-api-key': 'YOUR_API_KEY' } }
      );
      
      const data = await response.json();
      
      // Stop polling when complete
      if (data.status === 'COMPLETED' && data.findings) {
        setFindings(data.findings);
        setStatus('completed');
        clearInterval(pollInterval);
      }
    }, 30000); // Poll every 30 seconds
    
    // Cleanup after 30 minutes
    setTimeout(() => clearInterval(pollInterval), 30 * 60 * 1000);
  };

  return (
    <div>
      {status === 'idle' && <button>Start Verification</button>}
      {status === 'submitting' && <p>Submitting...</p>}
      {status === 'polling' && <p>Waiting for results...</p>}
      {status === 'completed' && (
        <div>
          <h3>Verification Complete!</h3>
          <p>{findings.comments}</p>
          <p>Registered Title: {findings.isRegisteredTitledVerified ? '✓' : '✗'}</p>
        </div>
      )}
    </div>
  );
}

React Polling Hook

Production-ready React hook with automatic cleanup and timeout handling:

// JavaScript (React) polling for findings
import { useEffect, useState } from "react";

function useFindingsPolling(verificationId, apiKey, pollInterval = 30000) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!verificationId || !apiKey) return;
    
    let isMounted = true;
    let intervalId;
    let timeoutId;
    
    const fetchFindings = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const res = await fetch(
          `/api/verification-requests/${verificationId}/findings`,
          { headers: { "x-api-key": apiKey } }
        );
        
        if (!res.ok) throw new Error(`Error: ${res.status}`);
        
        const result = await res.json();
        if (isMounted) {
          setData(result);
          
          // Stop polling if completed
          if (result.status === 'COMPLETED' && result.findings) {
            clearInterval(intervalId);
            clearTimeout(timeoutId);
          }
        }
      } catch (err) {
        if (isMounted) setError(err.message || "Unknown error");
      } finally {
        if (isMounted) setLoading(false);
      }
    };

    fetchFindings();
    intervalId = setInterval(fetchFindings, pollInterval);
    
    // Stop polling after 30 minutes
    timeoutId = setTimeout(() => {
      clearInterval(intervalId);
      if (isMounted) setError('Polling timeout - verification may still be in progress');
    }, 30 * 60 * 1000);

    return () => {
      isMounted = false;
      clearInterval(intervalId);
      clearTimeout(timeoutId);
    };
  }, [verificationId, apiKey, pollInterval]);

  return { ...data, loading, error };
}

Error Handling

Client Errors (4xx)

  • 400 - Bad Request (missing required fields)
  • 401 - Unauthorized (invalid API key)
  • 404 - Not Found (invalid verification ID)
  • 429 - Too Many Requests (rate limit exceeded)

Server Errors (5xx)

  • 500 - Internal Server Error
  • 503 - Service Unavailable

Example Error Response

{
  "message": "Missing required fields: address and userId",
  "error": "Bad Request"
}

Need API Access?

Request your API keys (test and production) to start integrating with LandVerify.