PROFILES -> OTP -> COMPLETE PROFILE FETCHED * * Uses ABDM Sandbox / Production endpoints with RSA-OAEP encryption. */ class AbdmV3DirectHelper { protected string $clientId = ''; protected string $clientSecret = ''; protected string $sessionsUrl = 'https://dev.abdm.gov.in/api/hiecm/gateway/v3/sessions'; protected string $certUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/public/certificate'; protected string $searchUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/account/abha/search'; protected string $otpUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/login/request/otp'; protected string $verifyUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/login/verify'; protected string $cardUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/account/abha-card'; protected string $profileUrl = 'https://abhasbx.abdm.gov.in/abha/api/v3/profile/account'; public function __construct() { $this->loadSettings(); } protected function log(string $level, string $msg): void { if (function_exists('log_message')) { \log_message($level, $msg); } else { error_log("[AbdmV3DirectHelper] [{$level}] {$msg}"); } } protected function loadSettings(): void { try { if (class_exists('\Config\Database') && function_exists('config')) { $db = \Config\Database::connect(); if ($db->tableExists('hospital_setting')) { $rows = $db->table('hospital_setting') ->whereIn('s_name', ['ABDM_CLIENT_ID', 'ABDM_CLIENT_SECRET']) ->get()->getResultArray(); foreach ($rows as $r) { $name = trim((string) ($r['s_name'] ?? '')); $val = trim((string) ($r['s_value'] ?? '')); if ($name === 'ABDM_CLIENT_ID' && $val !== '') { $this->clientId = $val; } if ($name === 'ABDM_CLIENT_SECRET' && $val !== '') { $this->clientSecret = $val; } } } } } catch (\Throwable $e) { $this->log('warning', '[AbdmV3DirectHelper] loadSettings failed: ' . $e->getMessage()); } if (function_exists('env')) { $envClientId = env('abdm.directClientId', env('BRIDGE_SOURCE_CODE', '')); if (! empty($envClientId) && empty($this->clientId)) { $this->clientId = (string) $envClientId; } $envSecret = env('abdm.directClientSecret', ''); if (! empty($envSecret) && empty($this->clientSecret)) { $this->clientSecret = (string) $envSecret; } } } protected function generateUuid(): string { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); } protected function curlCall(string $url, string $method, array $body = [], array $headers = []): array { $ch = curl_init($url); $opts = [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json', 'Accept: application/json'], $headers), CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, ]; if ($method === 'POST') { $opts[CURLOPT_POST] = true; $opts[CURLOPT_POSTFIELDS] = json_encode($body); } elseif ($method !== 'GET') { $opts[CURLOPT_CUSTOMREQUEST] = $method; if (! empty($body)) { $opts[CURLOPT_POSTFIELDS] = json_encode($body); } } curl_setopt_array($ch, $opts); $raw = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); curl_close($ch); $json = json_decode((string) $raw, true); return [ 'code' => $code, 'raw' => (string) $raw, 'err' => $err, 'json' => is_array($json) ? $json : [], ]; } /** * Get or refresh gateway session Bearer token. */ public function getSessionToken(bool $forceRefresh = false): string { $cacheKey = 'abdm_v3_direct_session_token'; if (! $forceRefresh && function_exists('cache')) { try { $cached = cache()->get($cacheKey); if (is_string($cached) && trim($cached) !== '') { return trim($cached); } } catch (\Throwable $e) {} } $resp = $this->curlCall($this->sessionsUrl, 'POST', [ 'clientId' => $this->clientId, 'clientSecret' => $this->clientSecret, 'grantType' => 'client_credentials', ], [ 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-CM-ID: sbx', ]); $token = (string) ($resp['json']['accessToken'] ?? ''); $expiresIn = (int) ($resp['json']['expiresIn'] ?? 1200); if ($token !== '' && function_exists('cache')) { try { cache()->save($cacheKey, $token, max(300, $expiresIn - 120)); } catch (\Throwable $e) {} } return $token; } /** * Get or refresh ABDM RSA Public Certificate PEM. */ public function getPublicCertificatePem(bool $forceRefresh = false): string { $cacheKey = 'abdm_v3_direct_public_cert_pem'; if (! $forceRefresh && function_exists('cache')) { try { $cached = cache()->get($cacheKey); if (is_string($cached) && trim($cached) !== '') { return trim($cached); } } catch (\Throwable $e) {} } $token = $this->getSessionToken($forceRefresh); if ($token === '') { throw new \RuntimeException('Failed to obtain ABDM session token for certificate fetch.'); } $resp = $this->curlCall($this->certUrl, 'GET', [], [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); $rawKey = (string) ($resp['json']['publicKey'] ?? $resp['json']['Certificate'] ?? $resp['raw'] ?? ''); if (trim($rawKey) === '') { throw new \RuntimeException('ABDM public certificate is empty.'); } if (! str_contains($rawKey, '-----BEGIN')) { $pemKey = "-----BEGIN PUBLIC KEY-----\n" . chunk_split(trim($rawKey), 64, "\n") . "-----END PUBLIC KEY-----\n"; } else { $pemKey = $rawKey; } if (function_exists('cache')) { try { cache()->save($cacheKey, $pemKey, 86400); } catch (\Throwable $e) {} } return $pemKey; } /** * RSA-OAEP encrypt text using ABDM public key. */ public function encrypt(string $plainText): string { $pem = $this->getPublicCertificatePem(); $pubKey = openssl_pkey_get_public($pem); if ($pubKey === false) { $pem = $this->getPublicCertificatePem(true); $pubKey = openssl_pkey_get_public($pem); if ($pubKey === false) { throw new \RuntimeException('Invalid ABDM public key resource: ' . openssl_error_string()); } } $encrypted = ''; $ok = openssl_public_encrypt($plainText, $encrypted, $pubKey, OPENSSL_PKCS1_OAEP_PADDING); if (! $ok) { $pem = $this->getPublicCertificatePem(true); $pubKey = openssl_pkey_get_public($pem); $ok = openssl_public_encrypt($plainText, $encrypted, $pubKey, OPENSSL_PKCS1_OAEP_PADDING); if (! $ok) { throw new \RuntimeException('RSA-OAEP encryption failed: ' . openssl_error_string()); } } return base64_encode($encrypted); } /** * Step 1: Search ABHA profiles by 10-digit mobile number. * Upstream: POST /abha/api/v3/profile/account/abha/search * * @return array{ok: int, txn_id?: string, profiles?: array, error_text?: string} */ public function searchByMobile(string $mobile): array { $digits = preg_replace('/\D/', '', $mobile); if (strlen($digits) !== 10) { return ['ok' => 0, 'error_text' => 'Valid 10-digit mobile number is required.']; } try { $token = $this->getSessionToken(); if ($token === '') { return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.']; } $encMobile = $this->encrypt($digits); $resp = $this->curlCall($this->searchUrl, 'POST', [ 'scope' => ['search-abha'], 'mobile' => $encMobile, ], [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); // If 401 Unauthorized, refresh token and retry once if ($resp['code'] === 401) { $token = $this->getSessionToken(true); $resp = $this->curlCall($this->searchUrl, 'POST', [ 'scope' => ['search-abha'], 'mobile' => $encMobile, ], [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); } if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) { $firstBlock = is_array($resp['json']) && isset($resp['json'][0]) ? $resp['json'][0] : $resp['json']; $txnId = (string) ($firstBlock['txnId'] ?? ''); $rawProfiles = (array) ($firstBlock['ABHA'] ?? $firstBlock['accounts'] ?? []); $profiles = []; foreach ($rawProfiles as $p) { $methods = []; foreach ((array) ($p['authMethods'] ?? []) as $m) { $mUpper = strtoupper(trim((string) $m)); if ($mUpper !== '' && ! in_array($mUpper, $methods, true)) { $methods[] = $mUpper; } } if (empty($methods)) { $methods = ['MOBILE_OTP', 'AADHAAR_OTP']; } $profiles[] = [ 'index' => (string) ($p['index'] ?? (count($profiles) + 1)), 'abha_number' => (string) ($p['ABHANumber'] ?? $p['abhaNumber'] ?? $p['abha_id'] ?? ''), 'name' => (string) ($p['name'] ?? ''), 'gender' => (string) ($p['gender'] ?? ''), 'dob' => (string) ($p['dob'] ?? $p['dateOfBirth'] ?? ''), 'kyc_verified' => (string) ($p['kycVerified'] ?? 'true') === 'true', 'auth_methods' => $methods, 'profile_photo' => (string) ($p['profilePhoto'] ?? ''), ]; } return [ 'ok' => 1, 'txn_id' => $txnId, 'profiles' => $profiles, ]; } $errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'ABDM search returned no accounts for this mobile.'); return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']]; } catch (\Throwable $e) { $this->log('error', '[AbdmV3DirectHelper] searchByMobile exception: ' . $e->getMessage()); return ['ok' => 0, 'error_text' => $e->getMessage()]; } } /** * Step 2: Request OTP for a selected profile index. * Upstream: POST /abha/api/v3/profile/login/request/otp * * @return array{ok: int, txn_id?: string, message?: string, error_text?: string} */ public function requestProfileOtp(string $searchTxnId, string $index, string $authMethod = 'MOBILE_OTP'): array { $authMethod = strtoupper(trim($authMethod)); $isAadhaar = in_array($authMethod, ['AADHAAR_OTP', 'AADHAAR', 'AADHAAR-VERIFY'], true); try { $token = $this->getSessionToken(); if ($token === '') { return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.']; } $encIndex = $this->encrypt((string) $index); $scope = $isAadhaar ? ['abha-login', 'search-abha', 'aadhaar-verify'] : ['abha-login', 'search-abha', 'mobile-verify']; $otpSystem = $isAadhaar ? 'aadhaar' : 'abdm'; $payload = [ 'scope' => $scope, 'loginHint' => 'index', 'loginId' => $encIndex, 'otpSystem' => $otpSystem, 'txnId' => $searchTxnId, ]; $resp = $this->curlCall($this->otpUrl, 'POST', $payload, [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); if ($resp['code'] === 401) { $token = $this->getSessionToken(true); $resp = $this->curlCall($this->otpUrl, 'POST', $payload, [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); } if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) { $newTxnId = (string) ($resp['json']['txnId'] ?? $searchTxnId); $msg = (string) ($resp['json']['message'] ?? 'OTP sent successfully.'); return [ 'ok' => 1, 'txn_id' => $newTxnId, 'message' => $msg, ]; } $errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'Failed to request OTP from ABDM.'); return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']]; } catch (\Throwable $e) { $this->log('error', '[AbdmV3DirectHelper] requestProfileOtp exception: ' . $e->getMessage()); return ['ok' => 0, 'error_text' => $e->getMessage()]; } } /** * Step 3: Verify the OTP and obtain full profile + tokens. * Upstream: POST /abha/api/v3/profile/login/verify * * @return array{ok: int, token?: string, accounts?: array, profile?: array, error_text?: string} */ public function verifyProfileOtp(string $otpTxnId, string $otp, string $authMethod = 'MOBILE_OTP'): array { $digits = preg_replace('/\D/', '', $otp); if (strlen($digits) !== 6) { return ['ok' => 0, 'error_text' => 'Enter a valid 6-digit OTP.']; } $authMethod = strtoupper(trim($authMethod)); $isAadhaar = in_array($authMethod, ['AADHAAR_OTP', 'AADHAAR', 'AADHAAR-VERIFY'], true); try { $token = $this->getSessionToken(); if ($token === '') { return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.']; } $encOtp = $this->encrypt($digits); $scope = $isAadhaar ? ['abha-login', 'aadhaar-verify'] : ['abha-login', 'mobile-verify']; $payload = [ 'scope' => $scope, 'authData' => [ 'authMethods' => ['otp'], 'otp' => [ 'txnId' => $otpTxnId, 'otpValue' => $encOtp, ], ], ]; $resp = $this->curlCall($this->verifyUrl, 'POST', $payload, [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); if ($resp['code'] === 401) { $token = $this->getSessionToken(true); $resp = $this->curlCall($this->verifyUrl, 'POST', $payload, [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); } if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) { $authResult = strtolower((string) ($resp['json']['authResult'] ?? '')); if ($authResult === 'failed') { $failMsg = (string) ($resp['json']['message'] ?? 'Invalid OTP entered.'); return ['ok' => 0, 'error_text' => $failMsg]; } $userToken = (string) ($resp['json']['token'] ?? ''); $accounts = (array) ($resp['json']['accounts'] ?? []); $firstAcc = isset($accounts[0]) && is_array($accounts[0]) ? $accounts[0] : []; // Attempt to fetch full profile details and ABHA card using the userToken $fullProfile = []; $cardBase64 = ''; if ($userToken !== '') { $fullProfile = $this->fetchProfileDetails($userToken, $token); $cardBase64 = $this->fetchCardPng($userToken, $token); } return [ 'ok' => 1, 'txn_id' => (string) ($resp['json']['txnId'] ?? $otpTxnId), 'token' => $userToken, 'accounts' => $accounts, 'account' => $firstAcc, 'full_profile' => $fullProfile, 'card_base64' => $cardBase64, ]; } $errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'OTP verification failed.'); return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']]; } catch (\Throwable $e) { $this->log('error', '[AbdmV3DirectHelper] verifyProfileOtp exception: ' . $e->getMessage()); return ['ok' => 0, 'error_text' => $e->getMessage()]; } } /** * M1 12.1 VRFY_ABHA_401: Request OTP using 12-digit Aadhaar Number. * Upstream: POST /abha/api/v3/profile/login/request/otp * * @return array{ok: int, txn_id?: string, message?: string, error_text?: string, resend_after?: int} */ public function requestLoginOtpByAadhaar(string $aadhaarNumber): array { $digits = preg_replace('/\D/', '', $aadhaarNumber); if (strlen($digits) !== 12) { return ['ok' => 0, 'error_text' => 'Aadhaar Number is not valid.']; } try { $token = $this->getSessionToken(); if ($token === '') { return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.']; } $encAadhaar = $this->encrypt($digits); $payload = [ 'scope' => ['abha-login', 'aadhaar-verify'], 'loginHint' => 'aadhaar', 'loginId' => $encAadhaar, 'otpSystem' => 'aadhaar', ]; $resp = $this->curlCall($this->otpUrl, 'POST', $payload, [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) { $newTxnId = (string) ($resp['json']['txnId'] ?? ''); $msg = (string) ($resp['json']['message'] ?? 'OTP sent to Aadhaar linked mobile number.'); return [ 'ok' => 1, 'txn_id' => $newTxnId, 'message' => $msg, 'resend_after' => 60, ]; } $errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? ''); if ($errMsg === '') { $errMsg = 'Failed to request Aadhaar OTP from ABDM.'; } if (stripos($errMsg, 'invalid') !== false && stripos($errMsg, 'aadhaar') !== false) { $errMsg = 'Aadhaar Number is not valid.'; } return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']]; } catch (\Throwable $e) { log_message('error', '[AbdmV3DirectHelper] requestLoginOtpByAadhaar exception: ' . $e->getMessage()); return ['ok' => 0, 'error_text' => $e->getMessage()]; } } /** * M1 12.1 VRFY_ABHA_402, 403, 404: Verify Aadhaar OTP and fetch complete ABHA profile + card. * Upstream: POST /abha/api/v3/profile/login/verify * * @return array{ok: int, txn_id?: string, token?: string, accounts?: array, account?: array, full_profile?: array, card_base64?: string, no_abha_found?: bool, error_text?: string} */ public function verifyLoginOtpByAadhaar(string $otpTxnId, string $otp): array { $digits = preg_replace('/\D/', '', $otp); if (strlen($digits) !== 6) { return ['ok' => 0, 'error_text' => 'Enter a valid 6-digit OTP.']; } try { $token = $this->getSessionToken(); if ($token === '') { return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.']; } $encOtp = $this->encrypt($digits); $payload = [ 'scope' => ['abha-login', 'aadhaar-verify'], 'authData' => [ 'authMethods' => ['otp'], 'otp' => [ 'txnId' => $otpTxnId, 'otpValue' => $encOtp, ], ], ]; $resp = $this->curlCall($this->verifyUrl, 'POST', $payload, [ 'Authorization: Bearer ' . $token, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]); if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) { $authResult = strtolower((string) ($resp['json']['authResult'] ?? '')); if ($authResult === 'failed') { $failMsg = (string) ($resp['json']['message'] ?? 'Incorrect OTP.'); return ['ok' => 0, 'error_text' => $failMsg]; } $userToken = (string) ($resp['json']['token'] ?? ''); $accounts = (array) ($resp['json']['accounts'] ?? []); $firstAcc = isset($accounts[0]) && is_array($accounts[0]) ? $accounts[0] : []; // Check VRFY_ABHA_403: No ABHA user registered with this Aadhaar Number if (empty($userToken) && empty($accounts)) { return [ 'ok' => 0, 'no_abha_found' => true, 'error_text' => 'NO ABHA user registered with this Aadhaar Number', ]; } $fullProfile = []; $cardBase64 = ''; if ($userToken !== '') { $fullProfile = $this->fetchProfileDetails($userToken, $token); $cardBase64 = $this->fetchCardPng($userToken, $token); } return [ 'ok' => 1, 'txn_id' => (string) ($resp['json']['txnId'] ?? $otpTxnId), 'token' => $userToken, 'accounts' => $accounts, 'account' => $firstAcc, 'full_profile' => $fullProfile, 'card_base64' => $cardBase64, ]; } $errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? ''); // Check if error is VRFY_ABHA_403: ABHA does not exist for this Aadhaar if (stripos($errMsg, 'no abha') !== false || stripos($errMsg, 'not found') !== false || stripos($errMsg, 'not exist') !== false || stripos($errMsg, 'no user') !== false || stripos($errMsg, 'no account') !== false) { return [ 'ok' => 0, 'no_abha_found' => true, 'error_text' => 'NO ABHA user registered with this Aadhaar Number', 'raw' => $resp['raw'], ]; } if (stripos($errMsg, 'otp') !== false || stripos($errMsg, 'invalid') !== false || stripos($errMsg, 'incorrect') !== false) { $errMsg = 'Incorrect OTP'; } if ($errMsg === '') { $errMsg = 'OTP verification failed.'; } return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']]; } catch (\Throwable $e) { $this->log('error', '[AbdmV3DirectHelper] verifyLoginOtpByAadhaar exception: ' . $e->getMessage()); return ['ok' => 0, 'error_text' => $e->getMessage()]; } } /** * Fetch full profile details using the profile X-Token. * Upstream: GET /abha/api/v3/profile/account */ public function fetchProfileDetails(string $xToken, string $gatewayToken = ''): array { try { $token = $gatewayToken !== '' ? $gatewayToken : $this->getSessionToken(); $headers = [ 'X-Token: Bearer ' . $xToken, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]; if ($token !== '') { $headers[] = 'Authorization: Bearer ' . $token; } $resp = $this->curlCall($this->profileUrl, 'GET', [], $headers); return is_array($resp['json']) ? $resp['json'] : []; } catch (\Throwable $e) { return []; } } /** * Fetch official ABHA card base64 image using profile X-Token. * Upstream: GET /abha/api/v3/profile/account/abha-card */ public function fetchCardPng(string $xToken, string $gatewayToken = ''): string { try { $token = $gatewayToken !== '' ? $gatewayToken : $this->getSessionToken(); $headers = [ 'X-Token: Bearer ' . $xToken, 'REQUEST-ID: ' . $this->generateUuid(), 'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'), 'X-Client-ID: ' . $this->clientId, ]; if ($token !== '') { $headers[] = 'Authorization: Bearer ' . $token; } $ch = curl_init($this->cardUrl); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_HTTPHEADER => $headers, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, ]); $raw = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code >= 200 && $code < 300 && ! empty($raw)) { // If returned as raw image binary, base64 encode it if (! str_contains($raw, '{')) { return base64_encode($raw); } // If returned as JSON with base64 $decoded = json_decode($raw, true); if (is_array($decoded)) { return (string) ($decoded['card'] ?? $decoded['card_base64'] ?? ''); } } } catch (\Throwable $e) {} return ''; } }