├── .gitignore ├── .phpunit.result.cache ├── composer.json ├── Exception └── ResponseException.php ├── phpunit.xml.dist ├── AbstractWhatsAppBusinessApiClient.php ├── Tests └── WhatsAppBusinessApiClientTest.php ├── README.md ├── WhatsAppBusinessApiSyncClient.php └── WhatsAppBusinessApiClient.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | test*.php -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":297:{a:2:{s:7:"defects";a:2:{s:35:"WhatsAppBusinessApiClientTest::test";i:5;s:48:"Cayeye\Tests\WhatsAppBusinessApiClientTest::test";i:4;}s:5:"times";a:2:{s:35:"WhatsAppBusinessApiClientTest::test";d:0.025999999999999999;s:48:"Cayeye\Tests\WhatsAppBusinessApiClientTest::test";d:0.0060000000000000001;}}} -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cayeye/whatsapp-business-api-client", 3 | "type": "library", 4 | "description": "A HTTP client for the WhatsApp Business API", 5 | "license": "MIT", 6 | "require": { 7 | "php": "^7.2", 8 | "ext-json": "*", 9 | "symfony/mime": "^4.3", 10 | "symfony/http-client": "^4.3" 11 | }, 12 | "autoload": { 13 | "psr-4": { "Cayeye\\WhatsAppBusinessApiClient\\": "" }, 14 | "exclude-from-classmap": [ 15 | "/Tests/" 16 | ] 17 | }, 18 | "minimum-stability": "dev", 19 | "extra": { 20 | "branch-alias": { 21 | "dev-master": "0.1-dev" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Exception/ResponseException.php: -------------------------------------------------------------------------------- 1 | response = $response; 20 | 21 | parent::__construct($message, $code); 22 | } 23 | 24 | /** 25 | * @return ResponseInterface 26 | */ 27 | public function getResponse(): ResponseInterface 28 | { 29 | return $this->response; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./Tests/ 18 | 19 | 20 | 21 | 22 | 23 | ./ 24 | 25 | ./Tests 26 | ./vendor 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /AbstractWhatsAppBusinessApiClient.php: -------------------------------------------------------------------------------- 1 | config = [ 22 | 'base_uri' => $url, 23 | 'auth_bearer' => $authBearer, 24 | 'headers' => [ 25 | 'Accept' => 'application/json', 26 | ], 27 | ]; 28 | } 29 | 30 | public function request(string $method, string $url, array $options = []): ResponseInterface 31 | { 32 | return $this->getClient()->request(strtoupper($method), $url, $options); 33 | } 34 | 35 | public function setClient(HttpClientInterface $client): void 36 | { 37 | $this->client = $client; 38 | } 39 | 40 | /** 41 | * @param callable|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory 42 | * @param string|null $baseUri 43 | */ 44 | public static function setMockClient($responseFactory = null, string $baseUri = null): void 45 | { 46 | self::$staticClient = new MockHttpClient($responseFactory, $baseUri); 47 | } 48 | 49 | protected function getClient(): HttpClientInterface 50 | { 51 | if (null === $this->client) { 52 | $this->client = self::$staticClient ?? HttpClient::create($this->config); 53 | } 54 | 55 | return self::$staticClient ?? $this->client; 56 | } 57 | } -------------------------------------------------------------------------------- /Tests/WhatsAppBusinessApiClientTest.php: -------------------------------------------------------------------------------- 1 | httpClient = $this->getMockBuilder(HttpClientInterface::class)->getMock(); 19 | $this->waClient = new WhatsAppBusinessApiClient(); 20 | $this->waClient->setClient($this->httpClient); 21 | } 22 | 23 | public function testSendMessage() 24 | { 25 | $userId = '123'; 26 | $text = 'foobar'; 27 | 28 | $this->httpClient->expects($this->once()) 29 | ->method('request') 30 | ->with( 31 | 'POST', 32 | '/v1/messages', 33 | [ 34 | 'json' => [ 35 | 'text' => ['body' => $text], 36 | 'to' => $userId, 37 | 'type' => 'text', 38 | 'recipient_type' => 'individual', 39 | ], 40 | ] 41 | ); 42 | 43 | $this->waClient->sendMessage($userId, $text); 44 | } 45 | 46 | public function testSendMessageWithContact() 47 | { 48 | $userId = '123'; 49 | $name = 'foobar'; 50 | $phone1 = '3434343434'; 51 | $phone2 = '5656565656'; 52 | 53 | $this->httpClient->expects($this->once()) 54 | ->method('request') 55 | ->with( 56 | 'POST', 57 | '/v1/messages', 58 | [ 59 | 'json' => [ 60 | 'to' => $userId, 61 | 'recipient_type' => 'individual', 62 | 'type' => 'contacts', 63 | 'contacts' => [ 64 | [ 65 | 'name' => ['formatted_name' => $name], 66 | 'phones' => [ 67 | ['phone' => $phone1], 68 | ['phone' => $phone2], 69 | ], 70 | ], 71 | ], 72 | ], 73 | ] 74 | ); 75 | 76 | $this->waClient->sendMessageWithContact($userId, $name, [$phone1, $phone2]); 77 | } 78 | 79 | public function testGetProfilePhoto() 80 | { 81 | $this->httpClient->expects($this->once()) 82 | ->method('request') 83 | ->with( 84 | 'GET', 85 | '/v1/settings/profile/photo', 86 | [] 87 | ); 88 | 89 | $this->waClient->getProfilePhoto(true); 90 | } 91 | 92 | public function testGetProfilePhotoWithLink() 93 | { 94 | $this->httpClient->expects($this->once()) 95 | ->method('request') 96 | ->with( 97 | 'GET', 98 | '/v1/settings/profile/photo', 99 | [ 100 | 'format' => 'link' 101 | ] 102 | ); 103 | 104 | $this->waClient->getProfilePhoto(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WhatsApp Business Api Client 2 | 3 | ## Based on 4 | - https://developers.facebook.com/docs/whatsapp/api/reference 5 | - https://developers.facebook.com/docs/whatsapp/api/errors 6 | 7 | ## Usage 8 | ```php 9 | $token = 'YourAdminAuthToken'; 10 | $url = 'http://127.0.0.10:3000'; 11 | $client = new WhatsAppBusinessApiClient($token, $url); 12 | 13 | //set webhook 14 | $client->updateWebhook('https://yourdomain.com/webhook'); 15 | ``` 16 | 17 | ## Available endpoints 18 | 19 | #### Done 20 | ``` 21 | //media 22 | POST {{URL}}/v1/media 23 | GET {{URL}}/v1/media/{{Test-Media-Id}} 24 | DEL {{URL}}/v1/media/{{Test-Media-Id}} 25 | 26 | //Settings Application 27 | GET {{URL}}/v1/settings/application 28 | PATCH {{URL}}/v1/settings/application 29 | 30 | //Messages 31 | POST {{URL}}/v1/messages/ 32 | PUT {{URL}}/v1/messages/ 33 | 34 | //contacts 35 | POST {{URL}}/v1/contacts 36 | 37 | //health 38 | GET {{URL}}/v1/health 39 | 40 | //Settings Profile 41 | GET {{URL}}/v1/settings/profile/photo 42 | POST {{URL}}/v1/settings/profile/photo 43 | GET {{URL}}/v1/settings/profile/about 44 | PATCH {{URL}}/v1/settings/profile/about 45 | 46 | //Groups 47 | GET {{URL}}/v1/groups/ 48 | POST {{URL}}/v1/groups 49 | GET {{URL}}/v1/groups/{{Test-Group-Id}} 50 | PUT {{URL}}/v1/groups/{{Test-Group-Id}} 51 | GET {{URL}}/v1/groups/{{Test-Group-Id}}/icon 52 | POST {{URL}}/v1/groups/{{Test-Group-Id}}/icon 53 | DELETE {{URL}}/v1/groups/{{Test-Group-Id}}/icon 54 | GET {{URL}}/v1/groups/{{Test-Group-Id}}/invite 55 | DELETE {{URL}}/v1/groups/{{Test-Group-Id}}/invite 56 | POST {{URL}}/v1/groups/{{Test-Group-Id}}/leave 57 | DELETE {{URL}}/v1/groups/{{Test-Group-Id}}/participants 58 | PATCH {{URL}}/v1/groups/{{Test-Group-Id}}/admins 59 | DELETE {{URL}}/v1/groups/{{Test-Group-Id}}/admins 60 | ``` 61 | 62 | #### TODO 63 | ``` 64 | //Settings - Backup/Restore 65 | PATCH {{URL}}/v1/settings/restore 66 | PATCH {{URL}}/v1/settings/backup 67 | 68 | //Settings - Business Profile 69 | POST {{URL}}/v1/settings/business/profile 70 | GET {{URL}}/v1/settings/business/profile 71 | 72 | //Settings - two step verification 73 | DEL {{URL}}/v1/settings/account/two-step 74 | POST {{URL}}/v1/settings/account/two-step 75 | 76 | //Settings - Application 77 | DEL {{URL}}/v1/settings/application/media/providers/ 78 | GET {{URL}}/v1/settings/application/media/providers 79 | POST {{URL}}/v1/settings/application/media/providers 80 | DEL {{URL}}/v1/settings/application 81 | POST {{URL}}/v1/account/shards 82 | 83 | //Users 84 | POST {{URL}}/v1/users 85 | POST {{URL}}/v1/users/logout 86 | POST {{URL}}/v1/users/login 87 | GET {{URL}}/v1/users/{{UserUsername}} 88 | PUT {{URL}}/v1/users/{{UserUsername}} 89 | DELETE {{URL}}/v1/users/{{UserUsername}} 90 | 91 | //Reg 92 | POST {{URL}}/v1/account 93 | POST {{URL}}/v1/account/verify 94 | 95 | //support 96 | GET {{URL}}/v1/support 97 | 98 | //stats 99 | GET {{URL}}/v1/stats/db 100 | GET {{URL}}/v1/stats/db/internal 101 | GET {{URL}}/v1/stats/app 102 | GET {{URL}}/v1/stats/app/internal 103 | 104 | //certificates 105 | POST {{URL}}/v1/certificates/external 106 | GET {{URL}}/v1/certificates/external/ca 107 | POST {{URL}}/v1/certificates/webhooks/ca 108 | GET {{URL}}/v1/certificates/webhooks/ca 109 | DEL {{URL}}/v1/certificates/webhooks/ca 110 | ``` 111 | 112 | 113 | -------------------------------------------------------------------------------- /WhatsAppBusinessApiSyncClient.php: -------------------------------------------------------------------------------- 1 | config = [ 29 | 'base_uri' => $url, 30 | 'auth_bearer' => $authBearer, 31 | 'headers' => [ 32 | 'Accept' => 'application/json', 33 | ], 34 | ]; 35 | } 36 | 37 | /** 38 | * @param string $id 39 | * 40 | * @return ResponseInterface 41 | */ 42 | public function getMedia(string $id): ResponseInterface 43 | { 44 | return $this->getClient()->request('GET', sprintf('/v1/media/%s', $id)); 45 | } 46 | 47 | /** 48 | * @param string $binary 49 | * @param string $mimeType 50 | * 51 | * @return array|ResponseInterface|null 52 | */ 53 | public function uploadMedia(string $binary, string $mimeType) 54 | { 55 | $options = [ 56 | 'headers' => ['content-type' => $mimeType], 57 | 'body' => $binary, 58 | ]; 59 | 60 | return $this->request('POST', '/v1/media', $options); 61 | } 62 | 63 | /** 64 | * @return ResponseInterface|array|null 65 | */ 66 | public function getSettings() 67 | { 68 | return $this->request('GET', '/v1/settings/application'); 69 | } 70 | 71 | /** 72 | * @param string $url 73 | * 74 | * @return ResponseInterface|array|null 75 | */ 76 | public function updateWebhook(string $url) 77 | { 78 | $data = [ 79 | 'webhooks' => ['url' => $url], 80 | ]; 81 | 82 | $options = [ 83 | 'json' => $data, 84 | ]; 85 | 86 | return $this->request('PATCH', '/v1/settings/application', $options); 87 | } 88 | 89 | /** 90 | * @param string $method 91 | * @param string $url 92 | * @param array $options 93 | * 94 | * @return ResponseInterface|array|null 95 | */ 96 | public function request(string $method, string $url, array $options = []) 97 | { 98 | $response = $this->getClient()->request(strtoupper($method), $url, $options); 99 | 100 | if (false === $this->async) { 101 | return self::computeResponse($response); 102 | } else { 103 | return $response; 104 | } 105 | } 106 | 107 | public function setAsync(bool $async): void 108 | { 109 | $this->async = $async; 110 | } 111 | 112 | /** 113 | * @param ResponseInterface $response 114 | * 115 | * @return array|string|null 116 | */ 117 | public static function computeResponse(ResponseInterface $response) 118 | { 119 | $headers = $response->getHeaders(); 120 | 121 | $isJson = 0 === strpos($contentType = implode(';', $headers['content-type'] ?? ''), 'application/json'); 122 | 123 | $responseData = $isJson ? $response->toArray(false) : $response->getContent(false); 124 | 125 | if (400 <= $response->getStatusCode()) { 126 | throw new ResponseException($response); 127 | } 128 | 129 | return $responseData; 130 | } 131 | 132 | /** 133 | * @param callable|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory 134 | * @param string|null $baseUri 135 | */ 136 | public static function setMockClient($responseFactory = null, string $baseUri = null): void 137 | { 138 | self::$staticClient = new MockHttpClient($responseFactory, $baseUri); 139 | } 140 | 141 | private function getClient(): HttpClientInterface 142 | { 143 | if (null === $this->client) { 144 | $this->client = self::$staticClient ?? HttpClient::create($this->config); 145 | } 146 | 147 | return self::$staticClient ?? $this->client; 148 | } 149 | } -------------------------------------------------------------------------------- /WhatsAppBusinessApiClient.php: -------------------------------------------------------------------------------- 1 | 'link', 17 | ]; 18 | 19 | if ($binary) { 20 | unset($query['format']); 21 | } 22 | 23 | return $this->request('GET', '/v1/settings/profile/photo', $query); 24 | } 25 | 26 | public function uploadProfilePhoto(string $rawData): ResponseInterface 27 | { 28 | $options = [ 29 | 'body' => $rawData, 30 | ]; 31 | 32 | return $this->request('POST', '/v1/settings/profile/photo', $options); 33 | } 34 | 35 | public function deleteProfilePhoto(): ResponseInterface 36 | { 37 | return $this->request('DELETE', '/v1/settings/profile/photo'); 38 | } 39 | 40 | public function getProfileAbout(): ResponseInterface 41 | { 42 | return $this->request('GET', '/v1/settings/profile/about'); 43 | } 44 | 45 | public function updateProfileAbout(string $text): ResponseInterface 46 | { 47 | $data = [ 48 | 'text' => $text, 49 | ]; 50 | 51 | return $this->request('PATCH', '/v1/settings/profile/about', ['json' => $data]); 52 | } 53 | 54 | public function getContacts(array $contacts): ResponseInterface 55 | { 56 | $data = [ 57 | 'contacts' => $contacts, 58 | 'blocking' => 'wait', 59 | ]; 60 | 61 | return $this->request('POST', '/v1/contacts', ['json' => $data]); 62 | } 63 | 64 | public function sendMessage(string $to, string $text): ResponseInterface 65 | { 66 | $options = [ 67 | 'text' => ['body' => $text], 68 | # 'preview_url' => true, #:TODO: add check if text has url 69 | ]; 70 | 71 | $data = $this->buildMessageData($to, 'text', $options); 72 | 73 | return $this->request('POST', '/v1/messages', ['json' => $data]); 74 | } 75 | 76 | #string $providerName = null, 77 | public function sendMessageWithMedia(string $to, string $type, string $mediaIdOrUrl, string $text = null): ResponseInterface 78 | { 79 | if (!in_array($type, ['audio', 'image', 'video', 'document'])) { 80 | throw new \Exception('unknown type for media, available types are: audio|image|video|document'); 81 | } 82 | 83 | $isHttp = 0 === strpos($mediaIdOrUrl, 'http'); 84 | 85 | $optionsType = [ 86 | ($isHttp ? 'link' : 'id') => $mediaIdOrUrl, 87 | ]; 88 | 89 | if (!empty($text)) { 90 | $optionsType['caption'] = $text; 91 | } 92 | 93 | #if (null !== $providerName && $isHttp) { 94 | # $optionsType['provider'] = ['name' => $providerName]; 95 | #} 96 | 97 | $data = $this->buildMessageData($to, $type, [$type => $optionsType]); 98 | 99 | return $this->request('POST', '/v1/messages', ['json' => $data]); 100 | } 101 | 102 | public function sendMessageWithDocument(string $to, string $mediaIdOrUrl, string $fileName, string $text = null): ResponseInterface 103 | { 104 | $isHttp = 0 === strpos($mediaIdOrUrl, 'http'); 105 | 106 | $optionsType = [ 107 | ($isHttp ? 'link' : 'id') => $mediaIdOrUrl, 108 | 'filename' => $fileName, 109 | ]; 110 | 111 | if (!empty($text)) { 112 | $optionsType['caption'] = $text; 113 | } 114 | 115 | $data = $this->buildMessageData($to, 'document', ['document' => $optionsType]); 116 | 117 | return $this->request('POST', '/v1/messages', ['json' => $data]); 118 | } 119 | 120 | /** 121 | * @param string $to 122 | * @param string $name 123 | * @param string|array $phones 124 | * 125 | * @return ResponseInterface 126 | */ 127 | public function sendMessageWithContact(string $to, string $name, $phones): ResponseInterface 128 | { 129 | if(is_scalar($phones)) { 130 | $phones = [$phones]; 131 | } 132 | if(!is_array($phones)) { 133 | throw new \Exception('argument phone must be a string or array'); 134 | } 135 | 136 | $contact = [ 137 | "name" => [ 138 | "formatted_name" => $name, 139 | #"first_name" => "", 140 | #"last_name" => "", 141 | ], 142 | "phones" => array_map( 143 | function (string $phone) { 144 | return [ 145 | 'phone' => $phone, 146 | #'type' => "", 147 | ]; 148 | }, 149 | $phones 150 | ), 151 | ]; 152 | 153 | $data = $this->buildMessageData($to, 'contacts', ['contacts' => [$contact]]); 154 | 155 | return $this->request('POST', '/v1/messages', ['json' => $data]); 156 | } 157 | 158 | public function sendMessageWithLocation(string $to, string $lat, string $long, string $name, string $address): ResponseInterface 159 | { 160 | $optionsType = [ 161 | 'latitude' => $lat, 162 | 'longitude' => $long, 163 | 'name' => $name, 164 | 'address' => $address, 165 | ]; 166 | 167 | $data = $this->buildMessageData($to, 'location', ['location' => $optionsType]); 168 | 169 | return $this->request('POST', '/v1/messages', ['json' => $data]); 170 | } 171 | 172 | private function buildMessageData(string $to, string $type, array $options) 173 | { 174 | $isGroup = false !== strpos($to, '-'); #:TODO: idendify 175 | 176 | return $options + [ 177 | 'to' => $to, 178 | 'type' => $type, 179 | 'recipient_type' => $isGroup ? 'group' : 'individual', 180 | ]; 181 | } 182 | 183 | public function readMessage(string $id): ResponseInterface 184 | { 185 | return $this->request('GET', sprintf('/v1/messages/%s', $id)); 186 | } 187 | 188 | public function getMedia(string $id): ResponseInterface 189 | { 190 | return $this->request('GET', sprintf('/v1/media/%s', $id)); 191 | } 192 | 193 | public function deleteMedia(string $id): ResponseInterface 194 | { 195 | return $this->request('DELETE', sprintf('/v1/media/%s', $id)); 196 | } 197 | 198 | public function uploadMedia(string $binary, string $mimeType): ResponseInterface 199 | { 200 | $options = [ 201 | 'headers' => ['content-type' => $mimeType], 202 | 'body' => $binary, 203 | ]; 204 | 205 | return $this->request('POST', '/v1/media', $options); 206 | } 207 | 208 | public function getGroups(): ResponseInterface 209 | { 210 | return $this->request('GET', '/v1/groups'); 211 | } 212 | 213 | public function createGroup(string $name): ResponseInterface 214 | { 215 | $data = [ 216 | 'subject' => $name, 217 | ]; 218 | 219 | return $this->request('POST', '/v1/groups', ['json' => $data]); 220 | } 221 | 222 | public function updateGroup(string $groupId, string $name): ResponseInterface 223 | { 224 | $data = [ 225 | 'subject' => $name, 226 | ]; 227 | 228 | return $this->request('PUT', sprintf('/v1/groups/%s', $groupId), ['json' => $data]); 229 | } 230 | 231 | public function getGroup(string $groupId): ResponseInterface 232 | { 233 | return $this->request('GET', sprintf('/v1/groups/%s', $groupId)); 234 | } 235 | 236 | public function getGroupIcon(string $groupId, $binary = false): ResponseInterface 237 | { 238 | $query = [ 239 | 'format' => 'link', 240 | ]; 241 | 242 | if ($binary) { 243 | unset($query['format']); 244 | } 245 | 246 | return $this->request('GET', sprintf('/v1/groups/%s/icon', $groupId), $query); 247 | } 248 | 249 | public function uploadGroupIcon(string $groupId, string $binary, string $mimeType): ResponseInterface 250 | { 251 | $options = [ 252 | 'headers' => ['content-type' => $mimeType], 253 | 'body' => $binary, 254 | ]; 255 | 256 | return $this->request('POST', sprintf('/v1/groups/%s/icon', $groupId), $options); 257 | } 258 | 259 | public function getGroupInvite(string $groupId): ResponseInterface 260 | { 261 | return $this->request('GET', sprintf('/v1/groups/%s/invite', $groupId)); 262 | } 263 | 264 | public function deleteGroupIcon(string $groupId): ResponseInterface 265 | { 266 | return $this->request('DELETE', sprintf('/v1/groups/%s/icon', $groupId)); 267 | } 268 | 269 | public function deleteGroupInvite(string $groupId): ResponseInterface 270 | { 271 | return $this->request('DELETE', sprintf('/v1/groups/%s/invite', $groupId)); 272 | } 273 | 274 | public function leaveGroup(string $groupId): ResponseInterface 275 | { 276 | return $this->request('POST', sprintf('/v1/groups/%s/leave', $groupId)); 277 | } 278 | 279 | public function removeGroupParticipants(string $groupId, array $waIds): ResponseInterface 280 | { 281 | $data = [ 282 | 'wa_ids' => $waIds, 283 | ]; 284 | 285 | return $this->request('DELETE', sprintf('/v1/groups/%s/participants', $groupId), ['json' => $data]); 286 | } 287 | 288 | public function addGroupAdmins(string $groupId, array $waIds): ResponseInterface 289 | { 290 | $data = [ 291 | 'wa_ids' => $waIds, 292 | ]; 293 | 294 | return $this->request('PATCH', sprintf('/v1/groups/%s/admins', $groupId), ['json' => $data]); 295 | } 296 | 297 | public function removeGroupAdmins(string $groupId, array $waIds): ResponseInterface 298 | { 299 | $data = [ 300 | 'wa_ids' => $waIds, 301 | ]; 302 | 303 | return $this->request('DELETE', sprintf('/v1/groups/%s/admins', $groupId), ['json' => $data]); 304 | } 305 | 306 | public function getSettings(): ResponseInterface 307 | { 308 | return $this->request('GET', '/v1/settings/application'); 309 | } 310 | 311 | public function updateSettings(array $settings): ResponseInterface 312 | { 313 | return $this->request('PATCH', '/v1/settings/application', ['json' => $settings]); 314 | } 315 | 316 | public function updateWebhook(string $url): ResponseInterface 317 | { 318 | return $this->updateSettings(['webhooks' => ['url' => $url]]); 319 | } 320 | 321 | public function getHealth(): ResponseInterface 322 | { 323 | return $this->request('GET', '/v1/health'); 324 | } 325 | } --------------------------------------------------------------------------------