From 0291bd6195af4bb2217f19e2de1f651e08a40189 Mon Sep 17 00:00:00 2001 From: vickytechkey Date: Sat, 17 Oct 2026 13:34:43 +0530 Subject: [PATCH] feat: add core verifier engine, serializers, tests, vendor client, and test portal template --- CODEPLUS_API_DOCUMENTATION.md | 101 ++++++++++++- templates/verifier/portal.html | 30 ++-- verifier/engine/base.py | 3 + verifier/serializers.py | 2 +- verifier/tests.py | 130 +++++++++++++++++ verifier/vendors/codeplus.py | 257 ++++++++++++++++++++++++++++++++- 6 files changed, 510 insertions(+), 13 deletions(-) diff --git a/CODEPLUS_API_DOCUMENTATION.md b/CODEPLUS_API_DOCUMENTATION.md index 593f373..8df01b6 100644 --- a/CODEPLUS_API_DOCUMENTATION.md +++ b/CODEPLUS_API_DOCUMENTATION.md @@ -61,7 +61,8 @@ Environment variables configured in `docverify/settings.py` / `.env`: | **Pan Verification (v5 Recommended)** | `POST /v5/pan-verify` | `pan`, `pan_v5`, `pan_verify` | Full PAN holder details with name, DOB, gender, masked Aadhaar & address. | | **Detailed PAN API** | `POST /v4/pan-detailed` | `pan_detailed` | Comprehensive PAN data including address, contact info, and profile. | | **PAN Advanced API** | `POST /v4/pan-advanced` | `pan_advanced` | Standard government database verification for PAN validity and taxpayer status. | -| **Bank Verification (Full Details)** | `POST /v4/bank-verify` | `bank_account` | Penny drop routing for account holder name, MICR, IFSC, and branch details. | +| **Bank Verify (Penny Drop - v5)** | `POST /v5/bank-verify-pd` | `bank_verify_pd`, `bank_verify_v5`, `bank_account` | Verifies bank account using account number and IFSC via Penny Drop; returns validity and account holder name. | +| **Bank Verification (Full Details - v4)** | `POST /v4/bank-verify` | `bank_account` (with `mode: 'v4'`) | Penny drop routing for account holder name, MICR, IFSC, and branch details. | | **Fetch GST By PAN** | `POST /v4/fetch-gst` | `gst_by_pan`, `fetch_gst` | Retrieves all registered GSTIN entities associated with a business PAN. | | **Fetch MSME By PAN** | `POST /v4/msme-pan` | `msme`, `msme_by_pan`, `udyam` | Retrieves Udyam registration number and enterprise details linked to a PAN. | | **GST Verification (v5)** | `POST /v5/gst-verify` | `gstin`, `gst`, `gst_verify` | Verifies single GSTIN and returns legal business name and active status. | @@ -258,10 +259,106 @@ curl --location 'https://apihub.services/v4/pan-advanced' \ --- -### Function 4: Bank Verification (Full Details) +### Function 4: Bank Verify (Penny Drop - v5 Recommended) + +The Bank Account Verification – PD API verifies a bank account using the account number and IFSC code via automated penny drop routing. It checks whether the account is valid and active, and returns the account holder name and verified bank account details. - **Engine Request:** ```bash +curl -X POST http://localhost:8092/api/verify/ \ + -H "Content-Type: application/json" \ + -d '{ + "document_type": "bank_verify_pd", + "data": { + "account_number": "123456787270", + "ifsc": "UTIB0000373" + } + }' +``` +*(Note: You can also use `document_type: "bank_account"`, which automatically defaults to v5 Penny Drop with fallback to v4).* + +- **Direct Upstream cURL:** +```bash +curl --location 'https://apihub.services/v5/bank-verify-pd' \ + --header 'X-Client-Api-ID: CAPI-YYR0YJIE0VMTDENY' \ + --header 'X-Client-Api-Secret: your_client_api_secret' \ + --header 'Content-Type: application/json' \ + --data '{ + "account_number": "123456787270", + "ifsc": "UTIB0000373" + }' +``` + +- **Client Input Parameters:** + +| Parameter | Type | Description | +| :--- | :--- | :--- | +| `account_number` | String | Bank account number to be verified. | +| `ifsc` | String | 11-character IFSC code of the bank branch. | + +- **Upstream Success Response (HTTP 200):** +```json +{ + "status": "success", + "result": { + "failure_reason": null, + "account_number": "********7270", + "user": null, + "is_valid": true, + "account_holder_name": "MANOJ PRASAD", + "account_ifsc": "UTIB0000373", + "npci_error_code": null + }, + "tranx_id": "9eeb9d58-04f8-4f8b-9608-99731850993f", + "timestamp": "2026-05-22T05:10:43.561554+00:00", + "chargeble": "true", + "user_consent": "true" +} +``` + +- **Engine Unified Response (HTTP 200):** +```json +{ + "reference_id": "9eeb9d58-04f8-4f8b-9608-99731850993f", + "is_success": true, + "status": "SUCCESS", + "document_type": "bank_verify_pd", + "successful_vendor": "codeplus", + "data": { + "document_type": "BANK_ACCOUNT", + "verification_mode": "penny_drop_v5", + "account_number": "123456787270", + "masked_account_number": "********7270", + "ifsc": "UTIB0000373", + "account_ifsc": "UTIB0000373", + "beneficiary_name": "MANOJ PRASAD", + "account_holder_name": "MANOJ PRASAD", + "is_valid": true, + "account_exists": true, + "failure_reason": null, + "npci_error_code": null, + "tranx_id": "9eeb9d58-04f8-4f8b-9608-99731850993f", + "timestamp": "2026-05-22T05:10:43.561554+00:00", + "chargeble": "true", + "user_consent": "true" + }, + "attempts_count": 1, + "attempts": [ + { + "attempt_number": 1, + "vendor_name": "codeplus", + "is_success": true, + "status": "SUCCESS", + "latency_ms": 380.2 + } + ] +} +``` + +--- + +### Function 4B: Bank Verification (Full Details - v4 Legacy) +```bash curl -X POST http://localhost:8092/api/verify/ \ -H "Content-Type: application/json" \ -d '{ diff --git a/templates/verifier/portal.html b/templates/verifier/portal.html index eea6f98..8aa7804 100644 --- a/templates/verifier/portal.html +++ b/templates/verifier/portal.html @@ -768,11 +768,11 @@ -
+
🏦
-
Bank Account (Penny Drop)
- POST /v4/bank-verify +
Bank Verify (Penny Drop - v5)
+ POST /v5/bank-verify-pd
@@ -990,15 +990,27 @@ { label: "Sample Valid PAN", data: { pan: "ABCDE1234F" } } ] }, - bank_account: { - title: "Bank Account Verification (Full Details)", - docType: "BANK", + bank_verify_pd: { + title: "Bank Account Verification – PD (Penny Drop - v5)", + docType: "BANK_ACCOUNT", fields: [ - { id: "account_number", label: "Bank Account Number", placeholder: "e.g. 10001234567890", type: "text", required: true, isCode: true }, - { id: "ifsc", label: "IFSC Code", placeholder: "e.g. SBIN0001234", type: "text", required: true, isCode: true, maxLength: 11 } + { id: "account_number", label: "Bank Account Number", placeholder: "e.g. 123456787270", type: "text", required: true, isCode: true }, + { id: "ifsc", label: "IFSC Code", placeholder: "e.g. UTIB0000373", type: "text", required: true, isCode: true, maxLength: 11 } ], quickFill: [ - { label: "Penny Drop Sample", data: { account_number: "123456789012", ifsc: "SBIN0000001" } }, + { label: "Penny Drop v5 Sample (UTIB0000373)", data: { account_number: "123456787270", ifsc: "UTIB0000373" } }, + { label: "HDFC Bank Sample", data: { account_number: "50100234567890", ifsc: "HDFC0000001" } } + ] + }, + bank_account: { + title: "Bank Account Verification (Full Details / Auto Waterfall)", + docType: "BANK_ACCOUNT", + fields: [ + { id: "account_number", label: "Bank Account Number", placeholder: "e.g. 123456787270", type: "text", required: true, isCode: true }, + { id: "ifsc", label: "IFSC Code", placeholder: "e.g. UTIB0000373", type: "text", required: true, isCode: true, maxLength: 11 } + ], + quickFill: [ + { label: "Penny Drop v5 Sample", data: { account_number: "123456787270", ifsc: "UTIB0000373" } }, { label: "HDFC Bank Sample", data: { account_number: "50100234567890", ifsc: "HDFC0000001" } } ] }, diff --git a/verifier/engine/base.py b/verifier/engine/base.py index b81a1fd..42bf220 100644 --- a/verifier/engine/base.py +++ b/verifier/engine/base.py @@ -15,6 +15,9 @@ class DocumentType(str, Enum): VOTER_ID = "voter_id" PASSPORT = "passport" BANK_ACCOUNT = "bank_account" + BANK_VERIFY_PD = "bank_verify_pd" + BANK_VERIFY_V5 = "bank_verify_v5" + BANK_PD = "bank_pd" GSTIN = "gstin" GST = "gst" GST_VERIFY = "gst_verify" diff --git a/verifier/serializers.py b/verifier/serializers.py index ecb8598..1be2c0b 100644 --- a/verifier/serializers.py +++ b/verifier/serializers.py @@ -6,7 +6,7 @@ class VerifyDocumentRequestSerializer(serializers.Serializer): """Input payload serializer for document verification API.""" document_type = serializers.CharField( required=True, - help_text="Type of document, e.g. 'pan', 'aadhaar', 'driving_license', 'voter_id', 'bank_account', 'gstin'", + help_text="Type of document, e.g. 'pan', 'aadhaar', 'driving_license', 'voter_id', 'bank_account', 'bank_verify_pd', 'gstin'", ) data = serializers.DictField( required=True, diff --git a/verifier/tests.py b/verifier/tests.py index 0a82b52..549e662 100644 --- a/verifier/tests.py +++ b/verifier/tests.py @@ -381,6 +381,136 @@ class CodeplusVendorTests(TestCase): self.assertFalse(result_missing_ifsc.is_success) self.assertEqual(result_missing_ifsc.error_code, "MISSING_IFSC") + def test_codeplus_bank_verify_pd_v5_success(self): + """Test successful Bank Verify (Penny Drop - v5) API.""" + mock_v5_response = { + "status": "success", + "result": { + "failure_reason": None, + "account_number": "********7270", + "user": None, + "is_valid": True, + "account_holder_name": "MANOJ PRASAD", + "account_ifsc": "UTIB0000373", + "npci_error_code": None, + }, + "tranx_id": "9eeb9d58-04f8-4f8b-9608-99731850993f", + "timestamp": "2026-05-22T05:10:43.561554+00:00", + "chargeble": "true", + "user_consent": "true", + } + + class MockResponse: + status_code = 200 + def json(self): + return mock_v5_response + + from unittest.mock import patch + with patch.object(self.codeplus.session, "post", return_value=MockResponse()) as mock_post: + result = self.codeplus.verify( + "bank_verify_pd", + {"account_number": "123456787270", "ifsc": "UTIB0000373"}, + ) + + self.assertTrue(result.is_success) + self.assertEqual(result.status, VerificationStatus.SUCCESS) + self.assertEqual(result.data["beneficiary_name"], "MANOJ PRASAD") + self.assertEqual(result.data["account_holder_name"], "MANOJ PRASAD") + self.assertEqual(result.data["account_ifsc"], "UTIB0000373") + self.assertEqual(result.data["masked_account_number"], "********7270") + self.assertEqual(result.data["tranx_id"], "9eeb9d58-04f8-4f8b-9608-99731850993f") + self.assertEqual(result.data["verification_mode"], "penny_drop_v5") + self.assertTrue(result.data["is_valid"]) + mock_post.assert_called_once() + call_url = mock_post.call_args[0][0] + self.assertTrue(call_url.endswith("/v5/bank-verify-pd")) + + def test_codeplus_bank_verify_pd_v5_invalid_account(self): + """Test Bank Verify (Penny Drop - v5) when bank rejects account validity.""" + mock_v5_failure = { + "status": "failed", + "result": { + "failure_reason": "Account Does Not Exist", + "account_number": "********9999", + "user": None, + "is_valid": False, + "account_holder_name": None, + "account_ifsc": "UTIB0000373", + "npci_error_code": "NPCI_ACC_INVALID", + }, + "tranx_id": "8eeb9d58-04f8-4f8b-9608-99731850991a", + "timestamp": "2026-05-22T05:10:43.561554+00:00", + "chargeble": "false", + "user_consent": "true", + } + + class MockResponse: + status_code = 200 + def json(self): + return mock_v5_failure + + from unittest.mock import patch + with patch.object(self.codeplus.session, "post", return_value=MockResponse()): + result = self.codeplus.verify( + "bank_verify_pd", + {"account_number": "999999999999", "ifsc": "UTIB0000373"}, + ) + + self.assertFalse(result.is_success) + self.assertEqual(result.status, VerificationStatus.FAILED) + self.assertEqual(result.error_code, "NPCI_ACC_INVALID") + self.assertEqual(result.error_message, "Account Does Not Exist") + self.assertFalse(result.data["is_valid"]) + + def test_codeplus_bank_verify_pd_v5_http_400(self): + """Test Bank Verify (Penny Drop - v5) HTTP 400 Bad Request error.""" + class MockResponse: + status_code = 400 + def json(self): + return {"message": "Invalid IFSC code format", "status": "error"} + + from unittest.mock import patch + with patch.object(self.codeplus.session, "post", return_value=MockResponse()): + result = self.codeplus.verify( + "bank_verify_pd", + {"account_number": "1234567890", "ifsc": "INVALID_IFSC"}, + ) + + self.assertFalse(result.is_success) + self.assertEqual(result.status, VerificationStatus.FAILED) + self.assertEqual(result.error_code, "HTTP_400") + self.assertIn("Invalid IFSC", result.error_message) + + def test_codeplus_bank_verify_pd_v5_doctype_aliases(self): + """Test aliases: bank_verify_v5 and bank_pd route to v5 Penny Drop.""" + mock_v5_response = { + "status": "success", + "result": { + "failure_reason": None, + "account_number": "********7270", + "is_valid": True, + "account_holder_name": "MANOJ PRASAD", + "account_ifsc": "UTIB0000373", + "npci_error_code": None, + }, + "tranx_id": "9eeb9d58-04f8-4f8b-9608-99731850993f", + } + + class MockResponse: + status_code = 200 + def json(self): + return mock_v5_response + + from unittest.mock import patch + with patch.object(self.codeplus.session, "post", return_value=MockResponse()): + res1 = self.codeplus.verify("bank_verify_v5", {"account_number": "123456787270", "ifsc": "UTIB0000373"}) + self.assertTrue(res1.is_success) + self.assertEqual(res1.data["account_holder_name"], "MANOJ PRASAD") + + res2 = self.codeplus.verify("bank_pd", {"account_number": "123456787270", "ifsc": "UTIB0000373"}) + self.assertTrue(res2.is_success) + self.assertEqual(res2.data["account_holder_name"], "MANOJ PRASAD") + def test_codeplus_fetch_gst_success(self): """Test successful fetch GST by PAN.""" mock_response_payload = { diff --git a/verifier/vendors/codeplus.py b/verifier/vendors/codeplus.py index 5769df9..9f7a10f 100644 --- a/verifier/vendors/codeplus.py +++ b/verifier/vendors/codeplus.py @@ -36,6 +36,9 @@ class CodeplusVendor(BaseVendor): DocumentType.VOTER_ID.value, DocumentType.PASSPORT.value, DocumentType.BANK_ACCOUNT.value, + DocumentType.BANK_VERIFY_PD.value, + DocumentType.BANK_VERIFY_V5.value, + DocumentType.BANK_PD.value, DocumentType.GSTIN.value, DocumentType.GST.value, DocumentType.GST_VERIFY.value, @@ -113,6 +116,9 @@ class CodeplusVendor(BaseVendor): DocumentType.VOTER_ID.value: self.verify_voter_id, DocumentType.PASSPORT.value: self.verify_passport, DocumentType.BANK_ACCOUNT.value: self.verify_bank_account, + DocumentType.BANK_VERIFY_PD.value: self.verify_bank_verify_pd_v5, + DocumentType.BANK_VERIFY_V5.value: self.verify_bank_verify_pd_v5, + DocumentType.BANK_PD.value: self.verify_bank_verify_pd_v5, DocumentType.GSTIN.value: self.verify_gstin, DocumentType.GST.value: self.verify_gstin, DocumentType.GST_VERIFY.value: self.verify_gstin, @@ -572,7 +578,256 @@ class CodeplusVendor(BaseVendor): def verify_bank_account(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult: """ - Bank Verification (Full Details) API: + Bank Account Verification Dispatcher: + - Defaults to Bank Verify (Penny Drop - v5) Recommended API (`verify_bank_verify_pd_v5`) + - If mode or version is 'v4', 'detailed', or 'full', routes to `verify_bank_account_v4` + - If v5 encounters an upstream retryable failure (5xx or timeout), falls back to v4 + """ + mode = str(data.get("mode", "")).lower() + version = str(data.get("version", "")).lower() + + if version == "v4" or mode in ("v4", "detailed", "full"): + return self.verify_bank_account_v4(data, files) + + # Primary execution: Bank Verify (Penny Drop - v5) + v5_result = self.verify_bank_verify_pd_v5(data, files) + if v5_result.is_success: + return v5_result + + # Fallback locally to v4 if upstream server error / timeout or if upstream returns v4 response structure + is_v4_payload = isinstance(v5_result.raw_response, dict) and "bank_data" in v5_result.raw_response.get("data", {}) + if v5_result.retryable or is_v4_payload: + logger.info("Codeplus Bank Verify fallback to v4 triggered (retryable=%s, is_v4=%s)...", v5_result.retryable, is_v4_payload) + v4_result = self.verify_bank_account_v4(data, files) + if v4_result.is_success: + return v4_result + + return v5_result + + def verify_bank_verify_pd_v5(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult: + """ + Bank Verify (Penny Drop - v5): + Endpoint: POST https://apihub.services/v5/bank-verify-pd + Payload: + { + "account_number": "", + "ifsc": "" + } + Headers: + X-Client-Api-ID: + X-Client-Api-Secret: + Content-Type: application/json + Description: + Verifies a bank account using account number and IFSC code via penny drop. + Returns validity, account holder name, and verified bank account details. + Response (HTTP 200): + { + "status": "success", + "result": { + "failure_reason": null, + "account_number": "********7270", + "user": null, + "is_valid": true, + "account_holder_name": "MANOJ PRASAD", + "account_ifsc": "UTIB0000373", + "npci_error_code": null + }, + "tranx_id": "9eeb9d58-04f8-4f8b-9608-99731850993f", + "timestamp": "2026-05-22T05:10:43.561554+00:00", + "chargeble": "true", + "user_consent": "true" + } + """ + endpoint = f"{self.base_url}/v5/bank-verify-pd" + + account_number = str( + data.get("account_number") + or data.get("bank_account") + or data.get("account_no") + or data.get("account") + or "" + ).strip() + + ifsc = str( + data.get("ifsc") + or data.get("ifsc_code") + or "" + ).strip().upper() + + if not account_number: + return VendorResult( + vendor_name=self.name, + is_success=False, + status=VerificationStatus.FAILED, + error_code="MISSING_ACCOUNT_NUMBER", + error_message="account_number is required for Bank Verify (Penny Drop - v5)", + retryable=False, + ) + + if not ifsc: + return VendorResult( + vendor_name=self.name, + is_success=False, + status=VerificationStatus.FAILED, + error_code="MISSING_IFSC", + error_message="ifsc code is required for Bank Verify (Penny Drop - v5)", + retryable=False, + ) + + payload = { + "account_number": account_number, + "ifsc": ifsc, + } + headers = self.get_headers() + + try: + response = self.session.post( + endpoint, + json=payload, + headers=headers, + timeout=self.timeout, + ) + + try: + raw_json = response.json() + except Exception: + raw_json = {"text": response.text, "status_code": response.status_code} + + api_status = str(raw_json.get("status", "")).lower() + result_block = raw_json.get("result") if isinstance(raw_json.get("result"), dict) else {} + is_valid = result_block.get("is_valid") + account_holder_name = result_block.get("account_holder_name") or "" + failure_reason = result_block.get("failure_reason") + npci_error_code = result_block.get("npci_error_code") + masked_acc = result_block.get("account_number") or ( + f"********{account_number[-4:]}" if len(account_number) >= 4 else account_number + ) + account_ifsc = result_block.get("account_ifsc") or ifsc + + if response.status_code == 200: + # Success condition: status == 'success' and is_valid is True + if api_status == "success" and (is_valid is True or (is_valid is None and account_holder_name and not failure_reason)): + normalized_result = { + "document_type": "BANK_ACCOUNT", + "verification_mode": "penny_drop_v5", + "account_number": account_number, + "masked_account_number": masked_acc, + "ifsc": ifsc, + "account_ifsc": account_ifsc, + "beneficiary_name": account_holder_name, + "account_holder_name": account_holder_name, + "is_valid": True, + "account_exists": True, + "failure_reason": None, + "npci_error_code": None, + "user": result_block.get("user"), + "tranx_id": raw_json.get("tranx_id"), + "transaction_id": raw_json.get("tranx_id"), + "timestamp": raw_json.get("timestamp"), + "chargeble": raw_json.get("chargeble"), + "user_consent": raw_json.get("user_consent"), + "bank_details": result_block, + } + + return VendorResult( + vendor_name=self.name, + is_success=True, + status=VerificationStatus.SUCCESS, + data=normalized_result, + raw_response=raw_json, + ) + + # HTTP 200 with verification failure or account invalid + err_code = str(npci_error_code or "INVALID_BANK_ACCOUNT") + err_message = failure_reason or raw_json.get("message") or "Bank account verification failed" + normalized_result = { + "document_type": "BANK_ACCOUNT", + "verification_mode": "penny_drop_v5", + "account_number": account_number, + "masked_account_number": masked_acc, + "ifsc": ifsc, + "account_ifsc": account_ifsc, + "beneficiary_name": account_holder_name, + "account_holder_name": account_holder_name, + "is_valid": False, + "account_exists": False, + "failure_reason": failure_reason, + "npci_error_code": npci_error_code, + "user": result_block.get("user"), + "tranx_id": raw_json.get("tranx_id"), + "transaction_id": raw_json.get("tranx_id"), + "timestamp": raw_json.get("timestamp"), + "chargeble": raw_json.get("chargeble"), + "user_consent": raw_json.get("user_consent"), + "bank_details": result_block, + } + + logger.warning( + "Codeplus Bank Verify PD v5 returned failure: npci_error=%s, failure_reason=%s", + npci_error_code, + failure_reason, + ) + + return VendorResult( + vendor_name=self.name, + is_success=False, + status=VerificationStatus.FAILED, + data=normalized_result, + raw_response=raw_json, + error_code=err_code, + error_message=err_message, + retryable=False, + ) + + # Upstream HTTP error status (400, 500, etc.) + is_server_error = response.status_code >= 500 + err_code = str(npci_error_code or f"HTTP_{response.status_code}") + err_message = failure_reason or raw_json.get("message") or raw_json.get("detail") or raw_json.get("error") or "Bank verification failed" + if isinstance(err_message, dict): + err_message = str(err_message) + + logger.warning( + "Codeplus Bank Verify PD v5 failed: status_code=%d, code=%s, message=%s", + response.status_code, + err_code, + err_message, + ) + + return VendorResult( + vendor_name=self.name, + is_success=False, + status=VerificationStatus.ERROR if is_server_error else VerificationStatus.FAILED, + data=raw_json if isinstance(raw_json, dict) else {}, + raw_response=raw_json, + error_code=err_code, + error_message=str(err_message), + retryable=is_server_error or response.status_code in (429, 408, 401, 403), + ) + + except requests.exceptions.Timeout as exc: + logger.warning("Codeplus Bank Verify PD v5 timed out: %s", exc) + return VendorResult( + vendor_name=self.name, + is_success=False, + status=VerificationStatus.ERROR, + error_code="TIMEOUT", + error_message=f"Request to Codeplus Bank Verify PD v5 timed out: {exc}", + retryable=True, + ) + except requests.exceptions.RequestException as exc: + logger.warning("Codeplus Bank Verify PD v5 network exception: %s", exc) + return VendorResult( + vendor_name=self.name, + is_success=False, + status=VerificationStatus.ERROR, + error_code="NETWORK_ERROR", + error_message=f"Network error connecting to Codeplus Bank Verify PD v5 API: {exc}", + retryable=True, + ) + + def verify_bank_account_v4(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult: + """ + Bank Verification (Full Details - v4) API: Endpoint: POST https://apihub.services/v4/bank-verify Payload: {