documentverification/verifier/vendors/codeplus.py
vickytechkey 6521343ea1
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s
feat: add Bank Verification Full Details (v4) and fetch bank_name and branch
2026-10-17 15:46:32 +05:30

1515 lines
65 KiB
Python

import logging
import os
from typing import Any, Dict, Optional
import requests
from django.conf import settings
from django.utils import timezone
from verifier.engine.base import (
BaseVendor,
DocumentType,
VendorResult,
VerificationStatus,
)
logger = logging.getLogger(__name__)
class CodeplusVendor(BaseVendor):
"""
Codeplus Verification Adapter (apihub.services).
Implements verification workflows for PAN Advanced, Aadhaar, Driving License,
Voter ID, Passport, Bank Account, GSTIN, and custom documents.
"""
name: str = "codeplus"
display_name: str = "Codeplus Verification Services"
priority: int = 10 # Lower number = higher priority in waterfall
is_active: bool = True
supported_documents = [
DocumentType.PAN.value,
DocumentType.PAN_V5.value,
DocumentType.PAN_VERIFY.value,
DocumentType.PAN_ADVANCED.value,
DocumentType.PAN_DETAILED.value,
DocumentType.AADHAAR.value,
DocumentType.DRIVING_LICENSE.value,
DocumentType.VOTER_ID.value,
DocumentType.PASSPORT.value,
DocumentType.BANK_ACCOUNT.value,
DocumentType.BANK_VERIFY_FULL.value,
DocumentType.BANK_VERIFY_V4.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,
DocumentType.GST_BY_PAN.value,
DocumentType.FETCH_GST.value,
DocumentType.MSME.value,
DocumentType.MSME_BY_PAN.value,
DocumentType.UDYAM.value,
DocumentType.CUSTOM.value,
]
def __init__(self, config: Optional[Dict[str, Any]] = None):
super().__init__(config=config)
# Base URL: default https://apihub.services
self.base_url = (
self.config.get("base_url")
or getattr(settings, "CODEPLUS_BASE_URL", None)
or os.environ.get("CODEPLUS_BASE_URL", "https://apihub.services")
).rstrip("/")
# Client Credentials
self.client_api_id = (
self.config.get("client_api_id")
or self.config.get("client_id")
or getattr(settings, "CODEPLUS_CLIENT_API_ID", None)
or os.environ.get("CODEPLUS_CLIENT_API_ID", "CAPI-YYR0YJIE0VMTDENY")
)
self.client_api_secret = (
self.config.get("client_api_secret")
or self.config.get("api_secret")
or getattr(settings, "CODEPLUS_CLIENT_API_SECRET", None)
or os.environ.get("CODEPLUS_CLIENT_API_SECRET", "")
)
self.timeout = int(self.config.get("timeout", 20))
self.session = requests.Session()
def get_headers(self) -> Dict[str, str]:
"""
Construct required HTTP headers for Codeplus / apihub.services API calls:
- X-Client-Api-ID
- X-Client-Api-Secret
- Content-Type: application/json
"""
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
if self.client_api_id:
headers["X-Client-Api-ID"] = self.client_api_id
if self.client_api_secret:
headers["X-Client-Api-Secret"] = self.client_api_secret
return headers
def execute_verification(
self,
document_type: str,
data: Dict[str, Any],
files: Optional[Dict[str, Any]] = None,
) -> VendorResult:
"""
Dispatches verification to specific document handlers.
"""
handlers = {
DocumentType.PAN.value: self.verify_pan,
DocumentType.PAN_V5.value: self.verify_pan_v5,
DocumentType.PAN_VERIFY.value: self.verify_pan_v5,
DocumentType.PAN_ADVANCED.value: self.verify_pan_advanced,
DocumentType.PAN_DETAILED.value: self.verify_pan_detailed,
DocumentType.AADHAAR.value: self.verify_aadhaar,
DocumentType.DRIVING_LICENSE.value: self.verify_driving_license,
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_FULL.value: self.verify_bank_account_v4,
DocumentType.BANK_VERIFY_V4.value: self.verify_bank_account_v4,
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,
DocumentType.GST_BY_PAN.value: self.fetch_gst_by_pan,
DocumentType.FETCH_GST.value: self.fetch_gst_by_pan,
DocumentType.MSME.value: self.fetch_msme_by_pan,
DocumentType.MSME_BY_PAN.value: self.fetch_msme_by_pan,
DocumentType.UDYAM.value: self.fetch_msme_by_pan,
}
handler = handlers.get(document_type, self.verify_generic)
return handler(data, files)
# ──────────────────────────────────────────────────────────────────────────
# PAN Verification (v5 Recommended, v4 Advanced, & v4 Detailed APIs)
# ──────────────────────────────────────────────────────────────────────────
def verify_pan(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Dispatches to:
- verify_pan_v5 (Recommended default)
- verify_pan_detailed (if mode == 'detailed' or version == 'v4_detailed')
- verify_pan_advanced (if mode == 'advanced' or version == 'v4_advanced')
"""
mode = str(data.get("mode", "")).lower()
version = str(data.get("version", "")).lower()
if data.get("detailed") is True or mode == "detailed" or version == "v4_detailed":
return self.verify_pan_detailed(data, files)
if mode == "advanced" or version == "v4_advanced":
return self.verify_pan_advanced(data, files)
# Default to v5 Recommended API
v5_result = self.verify_pan_v5(data, files)
if v5_result.is_success:
return v5_result
# If v5 encounters an error/retryable failure, fallback locally to v4 advanced
if v5_result.retryable:
v4_result = self.verify_pan_advanced(data, files)
if v4_result.is_success:
return v4_result
return v5_result
def verify_pan_v5(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Pan Verification (v5) Recommended API:
Endpoint: POST https://apihub.services/v5/pan-verify
Payload:
{
"pan": "<PAN_NUMBER>"
}
Headers:
X-Client-Api-ID: <id>
X-Client-Api-Secret: <secret>
Returns full holder details: name, dob, gender, aadhaar linking, address details, pan type.
"""
endpoint = f"{self.base_url}/v5/pan-verify"
pan_number = (
data.get("pan")
or data.get("pan_number")
or data.get("document_id")
or ""
).strip().upper()
if not pan_number:
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED,
error_code="MISSING_PAN",
error_message="pan is required for PAN verification",
retryable=False,
)
payload = {"pan": pan_number}
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}
data_block = raw_json.get("data") if isinstance(raw_json.get("data"), dict) else {}
error_block = raw_json.get("error") if isinstance(raw_json.get("error"), dict) else {}
is_success = (
response.status_code == 200
and not error_block
and (
raw_json.get("success") is True
or raw_json.get("status") == "SUCCESS"
or raw_json.get("statusCode") == 200
or data_block.get("success") is True
or data_block.get("status") == "SUCCESS"
)
)
if is_success:
normalized_result = {
"document_type": "PAN",
"pan": data_block.get("pan") or pan_number,
"pan_type": data_block.get("panType"),
"full_name": data_block.get("fullName") or data_block.get("name"),
"first_name": data_block.get("firstName"),
"middle_name": data_block.get("middleName"),
"last_name": data_block.get("lastName"),
"gender": data_block.get("gender"),
"dob": data_block.get("dob"),
"aadhaar_number": data_block.get("aadhaarNumber"),
"aadhaar_linked": data_block.get("aadhaarLinked"),
"address_details": data_block.get("addressDetails"),
"service": data_block.get("service", "PAN_VERIFICATION"),
"result_code": data_block.get("resultCode"),
"client_ref_num": data_block.get("clientRefNum"),
"provider_transaction_id": data_block.get("providerTransactionId"),
"transaction_id": data_block.get("transactionId") or raw_json.get("transactionId"),
"request_id": raw_json.get("requestId") or raw_json.get("request_id"),
"timestamp": raw_json.get("timestamp"),
"pan_details": data_block,
}
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=normalized_result,
raw_response=raw_json,
)
# Failure / Error handling
is_server_error = response.status_code >= 500
if error_block:
err_code = str(error_block.get("code") or f"HTTP_{response.status_code}")
err_message = error_block.get("message") or "PAN verification failed"
elif data_block:
err_code = str(data_block.get("resultCode") or data_block.get("code") or f"HTTP_{response.status_code}")
err_message = data_block.get("message") or raw_json.get("message") or "PAN verification failed"
else:
err_code = f"HTTP_{response.status_code}"
err_message = raw_json.get("message") or "PAN verification failed"
logger.warning(
"Codeplus PAN Verification (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=data_block,
raw_response=raw_json,
error_code=err_code,
error_message=err_message,
retryable=is_server_error or response.status_code in (429, 408, 401, 403),
)
except requests.exceptions.Timeout as exc:
logger.warning("Codeplus PAN Verification (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 PAN Verification (v5) timed out: {exc}",
retryable=True,
)
except requests.exceptions.RequestException as exc:
logger.warning("Codeplus PAN Verification (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 PAN v5 API: {exc}",
retryable=True,
)
def verify_pan_advanced(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
PAN Advanced API:
Endpoint: POST https://apihub.services/v4/pan-advanced
Payload: {"pan": "<PAN_NUMBER>"}
Headers:
X-Client-Api-ID: <id>
X-Client-Api-Secret: <secret>
"""
endpoint = f"{self.base_url}/v4/pan-advanced"
pan_number = (
data.get("pan")
or data.get("pan_number")
or data.get("document_id")
or ""
).strip().upper()
if not pan_number:
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED,
error_code="MISSING_PAN",
error_message="PAN number is required in input data",
retryable=False,
)
payload = {"pan": pan_number}
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}
data_block = raw_json.get("data") if isinstance(raw_json.get("data"), dict) else {}
code = str(data_block.get("code") or "")
message = data_block.get("message") or raw_json.get("message")
# Success condition: HTTP 200 and Code 1000 ("Valid PAN.")
if response.status_code == 200 and code == "1000":
pan_data = data_block.get("pan_data") or data_block
normalized_result = {
"document_type": pan_data.get("document_type", "PAN"),
"document_id": pan_data.get("document_id", pan_number),
"name": pan_data.get("name"),
"first_name": pan_data.get("first_name"),
"last_name": pan_data.get("last_name"),
"category": pan_data.get("category"),
"category_type": pan_data.get("category_type"),
"is_individual": pan_data.get("is_individual"),
"individual_tax_compliance_status": pan_data.get("individual_tax_compliance_status"),
"aadhaar_linked": pan_data.get("aadhaar_linked"),
"pan_details": pan_data,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
}
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=normalized_result,
raw_response=raw_json,
)
# Failure / Error handling (e.g. code 1004 "PAN does not exist")
is_server_error = response.status_code >= 500
err_code = code or f"HTTP_{response.status_code}"
err_message = message or f"PAN verification failed (Code: {err_code})"
logger.warning(
"Codeplus PAN Advanced verification 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=data_block,
raw_response=raw_json,
error_code=err_code,
error_message=err_message,
retryable=is_server_error or response.status_code in (429, 408, 401, 403),
)
except requests.exceptions.Timeout as exc:
logger.warning("Codeplus PAN verification 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 PAN API timed out: {exc}",
retryable=True,
)
except requests.exceptions.RequestException as exc:
logger.warning("Codeplus PAN verification 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 PAN API: {exc}",
retryable=True,
)
def verify_pan_detailed(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Detailed PAN API:
Endpoint: POST https://apihub.services/v4/pan-detailed
Payload: {"pan_number": "<PAN_NUMBER>"}
Headers:
X-Client-Api-ID: <id>
X-Client-Api-Secret: <secret>
Returns comprehensive details: name, dob, address data, email, mobile, gender, aadhaar_linked, taxpayer_profile
"""
endpoint = f"{self.base_url}/v4/pan-detailed"
pan_number = (
data.get("pan_number")
or data.get("pan")
or data.get("document_id")
or ""
).strip().upper()
if not pan_number:
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED,
error_code="MISSING_PAN",
error_message="pan_number is required in input data",
retryable=False,
)
payload = {"pan_number": pan_number}
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}
data_block = raw_json.get("data") if isinstance(raw_json.get("data"), dict) else {}
error_block = raw_json.get("error") if isinstance(raw_json.get("error"), dict) else {}
code = str(data_block.get("code") or "")
# Success condition: HTTP 200 and Code 1000 ("Valid PAN.") or valid data block
is_code_success = code in ("1000", "SUCCESS", "200")
is_body_success = response.status_code == 200 and not error_block and ("data" in raw_json and bool(data_block))
if is_code_success or is_body_success:
pan_data = data_block.get("pan_data") or data_block
normalized_result = {
"document_type": pan_data.get("document_type", "PAN"),
"document_id": pan_data.get("document_id", pan_number),
"name": pan_data.get("name"),
"first_name": pan_data.get("first_name"),
"last_name": pan_data.get("last_name"),
"category": pan_data.get("category"),
"date_of_birth": pan_data.get("date_of_birth"),
"masked_aadhaar_number": pan_data.get("masked_aadhaar_number"),
"address_data": pan_data.get("address_data"),
"email": pan_data.get("email"),
"phone": pan_data.get("phone"),
"gender": pan_data.get("gender"),
"aadhaar_linked": pan_data.get("aadhaar_linked"),
"taxpayer_profile": pan_data.get("taxpayer_profile"),
"pan_details": pan_data,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
}
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=normalized_result,
raw_response=raw_json,
)
# Failure / Error handling (e.g. HTTP 400 with {"error": {"code": "INVALID_PAN", "message": "Invalid PAN number."}})
is_server_error = response.status_code >= 500
if error_block:
err_code = str(error_block.get("code") or f"HTTP_{response.status_code}")
err_message = error_block.get("message") or f"Detailed PAN verification failed"
elif data_block:
err_code = str(data_block.get("code") or f"HTTP_{response.status_code}")
err_message = data_block.get("message") or f"Detailed PAN verification failed"
else:
err_code = f"HTTP_{response.status_code}"
err_message = raw_json.get("message") or "Detailed PAN verification failed"
logger.warning(
"Codeplus PAN Detailed verification 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=data_block,
raw_response=raw_json,
error_code=err_code,
error_message=err_message,
retryable=is_server_error or response.status_code in (429, 408, 401, 403),
)
except requests.exceptions.Timeout as exc:
logger.warning("Codeplus PAN Detailed verification 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 PAN Detailed API timed out: {exc}",
retryable=True,
)
except requests.exceptions.RequestException as exc:
logger.warning("Codeplus PAN Detailed verification 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 PAN Detailed API: {exc}",
retryable=True,
)
# ──────────────────────────────────────────────────────────────────────────
# Other Document Verification Handlers (Stubs ready for additional endpoints)
# ──────────────────────────────────────────────────────────────────────────
def verify_aadhaar(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
endpoint = f"{self.base_url}/v4/aadhaar-verify"
return self._send_generic_request(endpoint, payload=data, doc_label="Aadhaar")
def verify_driving_license(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
endpoint = f"{self.base_url}/v4/dl-verify"
return self._send_generic_request(endpoint, payload=data, doc_label="Driving License")
def verify_voter_id(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
endpoint = f"{self.base_url}/v4/voter-id-verify"
return self._send_generic_request(endpoint, payload=data, doc_label="Voter ID")
def verify_passport(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
endpoint = f"{self.base_url}/v4/passport-verify"
return self._send_generic_request(endpoint, payload=data, doc_label="Passport")
def verify_bank_account(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Bank Account Verification Dispatcher:
- Prioritizes Bank Verification (Full Details - v4) (`verify_bank_account_v4`)
which retrieves beneficiary name, bank_name, branch, and city via penny drop routing
- If mode or version is 'v5', routes to `verify_bank_verify_pd_v5`
- Seamless failover across v4 and v5
"""
mode = str(data.get("mode", "")).lower()
version = str(data.get("version", "")).lower()
if version in ("v5", "pd") or mode in ("v5", "penny_drop", "pd"):
return self.verify_bank_verify_pd_v5(data, files)
# Primary execution: Bank Verification (Full Details - v4) to fetch bank_name and branch
v4_result = self.verify_bank_account_v4(data, files)
if v4_result.is_success:
return v4_result
# Fallback to v5 Penny Drop if v4 failed with retryable/network error
if v4_result.retryable:
v5_result = self.verify_bank_verify_pd_v5(data, files)
if v5_result.is_success:
return v5_result
return v4_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": "<ACCOUNT_NUMBER>",
"ifsc": "<IFSC_CODE>"
}
Headers:
X-Client-Api-ID: <id>
X-Client-Api-Secret: <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
recommended_action = result_block.get("recommended_action") or raw_json.get("recommended_action")
def resolve_bank_failure_reason(code: Optional[str], raw_reason: Optional[str]) -> str:
if code in ("U01", "U1"):
return "Beneficiary bank reported: Account does not exist or invalid account number at this branch."
if code in ("U03", "U3"):
return "Beneficiary bank reported: Net debit cap exceeded / transaction limit reached."
if code in ("U09", "U9"):
return "Beneficiary bank server timed out. Please try again in a few moments."
if code in ("U13",):
return "Beneficiary bank reported: Account is dormant, inactive, or frozen."
if code in ("U28",):
return "Beneficiary bank reported: Branch does not participate in IMPS penny drop verification."
if code in ("U30",):
return "Beneficiary bank reported: Verification transaction declined by beneficiary bank."
if raw_reason and str(raw_reason).lower() not in ("bank verification failed", "failed", "invalid_bank_account"):
return str(raw_reason)
return "The destination bank could not verify this account. The account number or IFSC code is incorrect, inactive, or not accepting IMPS penny drop."
friendly_failure_reason = resolve_bank_failure_reason(npci_error_code, failure_reason or raw_json.get("message"))
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,
"recommended_action": recommended_action,
"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")
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": friendly_failure_reason,
"raw_failure_reason": failure_reason,
"npci_error_code": npci_error_code,
"recommended_action": recommended_action,
"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,
friendly_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=friendly_failure_reason,
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}")
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": friendly_failure_reason,
"raw_failure_reason": failure_reason or raw_json.get("message"),
"npci_error_code": npci_error_code,
"recommended_action": recommended_action,
"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 if result_block else raw_json,
}
logger.warning(
"Codeplus Bank Verify PD v5 failed: status_code=%d, code=%s, message=%s",
response.status_code,
err_code,
friendly_failure_reason,
)
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.ERROR if is_server_error else VerificationStatus.FAILED,
data=normalized_result,
raw_response=raw_json,
error_code=err_code,
error_message=friendly_failure_reason,
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:
{
"account_number": "<ACCOUNT_NUMBER>",
"ifsc": "<IFSC_CODE>"
}
Headers:
X-Client-Api-ID: <id>
X-Client-Api-Secret: <secret>
Retrieves account holder name via penny drop routing, MICR code, city, pincode, etc.
"""
endpoint = f"{self.base_url}/v4/bank-verify"
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 verification",
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 verification",
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}
data_block = raw_json.get("data") if isinstance(raw_json.get("data"), dict) else {}
error_block = raw_json.get("error") if isinstance(raw_json.get("error"), dict) else {}
code = str(data_block.get("code") or "")
bank_data = data_block.get("bank_account_data") or data_block.get("bank_data") or data_block or raw_json
beneficiary_name = (
bank_data.get("name")
or bank_data.get("name_at_bank")
or bank_data.get("beneficiary_name")
or bank_data.get("account_holder_name")
or data_block.get("name")
)
bank_name = bank_data.get("bank_name") or bank_data.get("bank")
branch = bank_data.get("branch") or bank_data.get("bank_branch")
city = bank_data.get("city")
utr = bank_data.get("utr")
account_status = bank_data.get("account_status")
# Success condition: HTTP 200 and either code 1000 or valid bank verification payload
is_code_success = code in ("1000", "SUCCESS", "200") or bool(beneficiary_name and response.status_code == 200 and not error_block)
if is_code_success and not error_block:
masked_acc = f"********{account_number[-4:]}" if len(account_number) >= 4 else account_number
normalized_result = {
"document_type": "BANK_ACCOUNT",
"verification_mode": "full_details_v4",
"account_number": account_number,
"masked_account_number": masked_acc,
"ifsc": ifsc,
"account_ifsc": ifsc,
"beneficiary_name": beneficiary_name,
"account_holder_name": beneficiary_name,
"bank_name": bank_name,
"branch": branch,
"bank_branch": branch,
"city": city,
"state": bank_data.get("state"),
"pincode": bank_data.get("pincode") or bank_data.get("pin_code"),
"micr": bank_data.get("micr") or bank_data.get("micr_code"),
"utr": utr,
"account_status": account_status,
"npci_response_code": bank_data.get("npci_response_code"),
"npci_description": bank_data.get("npci_description"),
"is_valid": True,
"account_exists": True,
"failure_reason": None,
"npci_error_code": None,
"user_consent": str(data.get("user_consent") or "true"),
"bank_details": bank_data,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
"tranx_id": raw_json.get("transaction_id") or raw_json.get("request_id"),
"timestamp": str(raw_json.get("timestamp") or timezone.now().isoformat()),
}
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=normalized_result,
raw_response=raw_json,
)
# Failure / Error handling (e.g. code 1001 "Provided invalid Account Number." or 400 "Invalid IFSC Code")
is_server_error = response.status_code >= 500
err_code = str(code or (error_block.get("code") if error_block else f"HTTP_{response.status_code}"))
err_message = data_block.get("message") or ""
if not err_message and error_block:
fields = error_block.get("metadata", {}).get("fields", [])
if fields and isinstance(fields, list):
field_msgs = [f.get("message") for f in fields if f.get("message")]
if field_msgs:
err_message = "; ".join(field_msgs)
if not err_message:
err_message = error_block.get("message") or "Bank verification failed"
if not err_message:
err_message = raw_json.get("message") or "Bank verification failed"
normalized_result = {
"document_type": "BANK_ACCOUNT",
"verification_mode": "full_details_v4",
"account_number": account_number,
"ifsc": ifsc,
"account_ifsc": ifsc,
"beneficiary_name": None,
"account_holder_name": None,
"bank_name": None,
"branch": None,
"bank_branch": None,
"is_valid": False,
"account_exists": False,
"failure_reason": err_message,
"npci_error_code": err_code,
"bank_details": data_block,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
}
logger.warning(
"Codeplus Bank verification (v4 Full Details) 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=normalized_result,
raw_response=raw_json,
error_code=err_code,
error_message=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 verification 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 verification timed out: {exc}",
retryable=True,
)
except requests.exceptions.RequestException as exc:
logger.warning("Codeplus Bank verification 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 API: {exc}",
retryable=True,
)
def fetch_gst_by_pan(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Fetch GST By PAN API:
Endpoint: POST https://apihub.services/v4/fetch-gst
Payload:
{
"pan": "<PAN_NUMBER>"
}
Headers:
X-Client-Api-ID: <id>
X-Client-Api-Secret: <secret>
Returns all GSTINs associated with a valid business PAN.
"""
endpoint = f"{self.base_url}/v4/fetch-gst"
pan = str(
data.get("pan")
or data.get("pan_number")
or data.get("document_id")
or ""
).strip().upper()
if not pan:
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED,
error_code="MISSING_PAN",
error_message="pan is required to fetch GSTIN details",
retryable=False,
)
payload = {"pan": pan}
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}
data_block = raw_json.get("data") if isinstance(raw_json.get("data"), dict) else {}
error_block = raw_json.get("error") if isinstance(raw_json.get("error"), dict) else {}
code = str(data_block.get("code") or "")
# Success condition: HTTP 200 and presence of results or valid code (e.g. 1002, 1000)
is_success_code = code in ("1000", "1001", "1002", "SUCCESS")
has_results = "results" in data_block or isinstance(raw_json.get("results"), list)
if response.status_code == 200 and not error_block and (is_success_code or has_results or "data" in raw_json):
results = data_block.get("results") if "results" in data_block else raw_json.get("results", [])
normalized_result = {
"document_type": "GST_BY_PAN",
"pan": pan,
"records_count": len(results),
"message": data_block.get("message") or f"Found {len(results)} records",
"results": results,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
}
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=normalized_result,
raw_response=raw_json,
)
# Failure / Error handling
is_server_error = response.status_code >= 500
if error_block:
err_code = str(error_block.get("code") or f"HTTP_{response.status_code}")
err_message = error_block.get("message") or "GST by PAN fetch failed"
elif data_block:
err_code = str(data_block.get("code") or f"HTTP_{response.status_code}")
err_message = data_block.get("message") or "GST by PAN fetch failed"
else:
err_code = f"HTTP_{response.status_code}"
err_message = raw_json.get("message") or "GST by PAN fetch failed"
logger.warning(
"Codeplus Fetch GST by PAN 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=data_block,
raw_response=raw_json,
error_code=err_code,
error_message=err_message,
retryable=is_server_error or response.status_code in (429, 408, 401, 403),
)
except requests.exceptions.Timeout as exc:
logger.warning("Codeplus Fetch GST by PAN 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 Fetch GST by PAN timed out: {exc}",
retryable=True,
)
except requests.exceptions.RequestException as exc:
logger.warning("Codeplus Fetch GST by PAN 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 Fetch GST API: {exc}",
retryable=True,
)
def verify_gstin(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
GST Verification (v5) API:
Endpoint: POST https://apihub.services/v5/gst-verify
Payload:
{
"gst_number": "<GSTIN>"
}
Headers:
X-Client-Api-ID: <id>
X-Client-Api-Secret: <secret>
Returns legal business name and current GST registration status.
"""
gst_number = str(
data.get("gst_number")
or data.get("gstin")
or data.get("gst")
or data.get("document_id")
or ""
).strip().upper()
# If a 10-character PAN is supplied instead of a 15-character GSTIN, route to fetch_gst_by_pan
if not gst_number and (data.get("pan") or data.get("pan_number")):
return self.fetch_gst_by_pan(data, files)
if len(gst_number) == 10 and gst_number[3] in "CPHFATBLJG":
return self.fetch_gst_by_pan({"pan": gst_number}, files)
if not gst_number:
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED,
error_code="MISSING_GST_NUMBER",
error_message="gst_number is required for GST verification",
retryable=False,
)
endpoint = f"{self.base_url}/v5/gst-verify"
payload = {"gst_number": gst_number}
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}
data_block = raw_json.get("data") if isinstance(raw_json.get("data"), dict) else {}
error_block = raw_json.get("error") if isinstance(raw_json.get("error"), dict) else {}
# Success condition: HTTP 200 and success == True or statusCode == 200
is_success = (
response.status_code == 200
and not error_block
and (raw_json.get("success") is True or raw_json.get("statusCode") == 200 or "legalName" in data_block)
)
if is_success:
normalized_result = {
"document_type": "GSTIN",
"gst_number": data_block.get("gstNumber") or gst_number,
"legal_name": data_block.get("legalName"),
"registration_status": data_block.get("registrationStatus"),
"service": data_block.get("service", "GST_VERIFICATION"),
"message": raw_json.get("message", "GST verification completed"),
"gst_details": data_block,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
}
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=normalized_result,
raw_response=raw_json,
)
# Failure / Error handling
is_server_error = response.status_code >= 500
if error_block:
err_code = str(error_block.get("code") or f"HTTP_{response.status_code}")
err_message = error_block.get("message") or "GST verification failed"
elif data_block:
err_code = str(data_block.get("code") or f"HTTP_{response.status_code}")
err_message = data_block.get("message") or raw_json.get("message") or "GST verification failed"
else:
err_code = f"HTTP_{response.status_code}"
err_message = raw_json.get("message") or "GST verification failed"
logger.warning(
"Codeplus GST Verification (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=data_block,
raw_response=raw_json,
error_code=err_code,
error_message=err_message,
retryable=is_server_error or response.status_code in (429, 408, 401, 403),
)
except requests.exceptions.Timeout as exc:
logger.warning("Codeplus GST verification 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 GST verification timed out: {exc}",
retryable=True,
)
except requests.exceptions.RequestException as exc:
logger.warning("Codeplus GST verification 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 GST API: {exc}",
retryable=True,
)
def fetch_msme_by_pan(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
"""
Fetch MSME By PAN API:
Endpoint: POST https://apihub.services/v4/msme-pan
Payload:
{
"pan": "<PAN_NUMBER>",
"detailed_response": true/false # (optional)
}
Headers:
X-Client-Api-ID: <id>
X-Client-Api-Secret: <secret>
Retrieves the Udyam Registration Number linked to a PAN and full Udyam details.
"""
endpoint = f"{self.base_url}/v4/msme-pan"
pan = str(
data.get("pan")
or data.get("pan_number")
or data.get("document_id")
or ""
).strip().upper()
if not pan:
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED,
error_code="MISSING_PAN",
error_message="pan is required to fetch MSME details",
retryable=False,
)
payload: Dict[str, Any] = {"pan": pan}
if data.get("detailed_response") is True or data.get("detailed") is True:
payload["detailed_response"] = True
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}
data_block = raw_json.get("data") if isinstance(raw_json.get("data"), dict) else {}
error_block = raw_json.get("error") if isinstance(raw_json.get("error"), dict) else {}
code = str(data_block.get("code") or "")
# Success condition: HTTP 200 and no error block
if response.status_code == 200 and not error_block:
# Code 1011 = "No record found."
has_record = code != "1011" and ("udyam" in data_block or "results" in data_block or code in ("1000", "1002"))
normalized_result = {
"document_type": "MSME",
"pan": pan,
"has_msme": has_record,
"code": code,
"message": data_block.get("message") or ("MSME record found" if has_record else "No record found."),
"udyam_data": data_block,
"request_id": raw_json.get("request_id"),
"transaction_id": raw_json.get("transaction_id"),
}
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=normalized_result,
raw_response=raw_json,
)
# Failure / Error handling
is_server_error = response.status_code >= 500
if error_block:
err_code = str(error_block.get("code") or f"HTTP_{response.status_code}")
err_message = error_block.get("message") or "MSME by PAN fetch failed"
elif data_block:
err_code = str(data_block.get("code") or f"HTTP_{response.status_code}")
err_message = data_block.get("message") or "MSME by PAN fetch failed"
else:
err_code = f"HTTP_{response.status_code}"
err_message = raw_json.get("message") or "MSME by PAN fetch failed"
logger.warning(
"Codeplus Fetch MSME by PAN 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=data_block,
raw_response=raw_json,
error_code=err_code,
error_message=err_message,
retryable=is_server_error or response.status_code in (429, 408, 401, 403),
)
except requests.exceptions.Timeout as exc:
logger.warning("Codeplus Fetch MSME by PAN 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 Fetch MSME by PAN timed out: {exc}",
retryable=True,
)
except requests.exceptions.RequestException as exc:
logger.warning("Codeplus Fetch MSME by PAN 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 Fetch MSME API: {exc}",
retryable=True,
)
def verify_generic(self, data: Dict[str, Any], files: Optional[Dict[str, Any]] = None) -> VendorResult:
endpoint = f"{self.base_url}/v4/document-verify"
return self._send_generic_request(endpoint, payload=data, doc_label="Generic Document")
def _send_generic_request(
self,
endpoint: str,
payload: Dict[str, Any],
doc_label: str = "Document",
) -> VendorResult:
"""Generic fallback dispatcher for endpoints awaiting specific contracts."""
headers = self.get_headers()
clean_payload = {k: v for k, v in payload.items() if v is not None}
try:
response = self.session.post(
endpoint,
json=clean_payload,
headers=headers,
timeout=self.timeout,
)
raw_json = response.json() if response.content else {}
if response.status_code == 200:
return VendorResult(
vendor_name=self.name,
is_success=True,
status=VerificationStatus.SUCCESS,
data=raw_json.get("data", raw_json),
raw_response=raw_json,
)
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.FAILED if response.status_code < 500 else VerificationStatus.ERROR,
raw_response=raw_json,
error_code=f"HTTP_{response.status_code}",
error_message=raw_json.get("message", f"Request failed with HTTP {response.status_code}"),
retryable=response.status_code >= 500,
)
except Exception as exc:
return VendorResult(
vendor_name=self.name,
is_success=False,
status=VerificationStatus.ERROR,
error_code=exc.__class__.__name__,
error_message=str(exc),
retryable=True,
)