token = $token; $this->isLive = $isLive; // Base URLs per OpenAPI Spec $this->baseUrl = $this->isLive ? 'https://apis.usps.com/prices/v3' : 'https://apis-tem.usps.com/prices/v3'; } /** * Get Domestic Rate (Rates v3) */ public function getDomesticRate($payload) { return $this->post('/base-rates/search', $payload); } /** * Get International Rate (Rates v3) */ public function getInternationalRate($payload) { // International endpoint uses a different base structure per spec $intlBaseUrl = $this->isLive ? 'https://apis.usps.com/international-prices/v3' : 'https://apis-tem.usps.com/international-prices/v3'; return $this->post('/base-rates/search', $payload, $intlBaseUrl); } /** * Internal POST logic using Symfony HTTP Client */ private function post($endpoint, $payload, $overrideUrl = null) { $url = ($overrideUrl ? $overrideUrl : $this->baseUrl) . $endpoint; $client = HttpClient::create([ 'timeout' => 15, 'verify_peer' => false, 'verify_host' => false, ]); try { $response = $client->request('POST', $url, [ 'headers' => [ 'Authorization' => 'Bearer ' . $this->token, 'Content-Type' => 'application/json', 'Accept' => 'application/json' ], 'json' => $payload ]); // toArray(false) prevents exception on 4xx/5xx responses so we can parse the error body $data = $response->toArray(false); $statusCode = $response->getStatusCode(); // Handle API Errors (400 Bad Request, 401 Unauthorized, etc) if ($statusCode >= 400) { $msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown Error'; // Try to extract deeper error detail (e.g., from 'errors' array) if (isset($data['error']['errors'][0]['detail'])) { $msg .= ' - ' . $data['error']['errors'][0]['detail']; } elseif (isset($data['error']['code'])) { $msg .= ' (' . $data['error']['code'] . ')'; } return ['error' => "API HTTP $statusCode: $msg"]; } return $data; } catch (TransportExceptionInterface $e) { return ['error' => 'Network/Transport Error: ' . $e->getMessage()]; } catch (\Exception $e) { return ['error' => 'Client Error: ' . $e->getMessage()]; } } }