token()) ->timeout($this->config['timeout']) ->{$method}("{$this->config['base_url']}{$path}", $payload); if ($response->status() === 401) { // Token expired early / was revoked — refresh once and retry. Cache::forget(self::TOKEN_CACHE_KEY); $response = Http::withToken($this->token()) ->timeout($this->config['timeout']) ->{$method}("{$this->config['base_url']}{$path}", $payload); } if ($response->failed()) { throw new BoxNowApiException( "Box Now API error ({$response->status()}) on {$method} {$path}", $response->json() ?? [], ); } return $response->json() ?? []; } /** * The origins/destinations lookups are served faster from a separate * location API host, per Box Now's own documentation. */ public function locationRequest(string $path, array $query = []): array { $response = Http::withToken($this->token()) ->timeout($this->config['timeout']) ->get("{$this->config['location_api_url']}{$path}", $query); if ($response->failed()) { throw new BoxNowApiException( "Box Now location API error ({$response->status()}) on GET {$path}", $response->json() ?? [], ); } return $response->json() ?? []; } /** * Fetch raw bytes (e.g. a PDF label) rather than JSON. */ public function requestRaw(string $path): string { $response = Http::withToken($this->token()) ->timeout($this->config['timeout']) ->get("{$this->config['base_url']}{$path}"); if ($response->failed()) { throw new BoxNowApiException("Box Now API error ({$response->status()}) on GET {$path}"); } return $response->body(); } private function token(): string { return Cache::remember(self::TOKEN_CACHE_KEY, now()->addMinutes(55), function () { $response = Http::timeout($this->config['timeout']) ->post("{$this->config['base_url']}/auth-sessions", [ 'grant_type' => 'client_credentials', 'client_id' => $this->config['client_id'], 'client_secret' => $this->config['client_secret'], ]); if ($response->failed()) { throw new BoxNowApiException('Box Now authentication failed', $response->json() ?? []); } return $response->json('access_token'); }); } }