├── .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 | }
--------------------------------------------------------------------------------