從 PHP 和 Laravel 傳送及接收 WhatsApp 訊息
接收只需一條 Laravel 路由:回應 Meta 的驗證 GET 請求,並接受 POST。由於 PHP 會把點號轉成底線,參數要以 hub_mode、hub_verify_token 和 hub_challenge 讀取。傳送則是一次 Http::withToken() POST,目標是 Cloud API 的 messages 端點,並帶上該號碼的電話號碼 ID 和存取權杖。無需任何 SDK。
PHP 會把傳入變數名稱中的點號轉成底線,所以 hub.challenge 要以 hub_challenge 讀取。
由 install:api 建立的 Laravel 無狀態 API 路由,位於 /api 之下,不受 web 群組的 CSRF 保護所限。
Graph API 版本取自 Meta 現行的傳送訊息範例,端點為 POST /{phone-number-id}/messages。
如何在 Laravel 接收 WhatsApp 訊息?
在 routes/api.php 的同一路徑上設定一條 GET 和一條 POST。如果該檔案不存在,請先執行 php artisan install:api。API 路由是無狀態的,不屬於 web 群組,所以 Meta 的 POST 不會因為缺少 CSRF 權杖而被拒絕,而放在 routes/web.php 的路由就會發生這種情況。
GET 負責回應 Meta 的驗證。Meta 會傳送 hub.mode、hub.verify_token 和 hub.challenge,但 PHP 會把傳入變數名稱中的點號改成底線,所以 Laravel 看到的是 hub_mode、hub_verify_token 和 hub_challenge。如果程式碼讀取帶點號的名稱,只會得到 null,驗證永遠不會通過。請以純文字回傳 challenge,不要使用 response()->json()。
POST 則把負載交給隊列作業,並立即回傳 200。請在路徑中加入一個很長的隨機金鑰,並在兩條路由上都檢查它:Meta 以 Tech Provider 的應用程式密鑰簽署每次傳送,所以您這邊無法驗證簽署標頭。
use App\Jobs\HandleWhatsAppEvent;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/whatsapp/{key}', function (Request $request, string $key) {
abort_unless(hash_equals(config('services.whatsapp.url_key'), $key), 404);
// PHP turns hub.mode into hub_mode: read the underscored names
if ($request->query('hub_mode') === 'subscribe'
&& hash_equals(config('services.whatsapp.verify_token'),
(string) $request->query('hub_verify_token'))) {
return response((string) $request->query('hub_challenge'), 200)
->header('Content-Type', 'text/plain');
}
abort(403);
});
Route::post('/whatsapp/{key}', function (Request $request, string $key) {
abort_unless(hash_equals(config('services.whatsapp.url_key'), $key), 404);
HandleWhatsAppEvent::dispatch($request->json()->all());
return response('', 200);
});如何從 Laravel 傳送 WhatsApp 訊息?
使用 Laravel 的 HTTP 用戶端:向該電話號碼 ID 的 messages 端點發出一次 POST,並以 Bearer 權杖形式帶上存取權杖。這兩個值都來自連接,可在控制台查看,也可由 get_api_credentials 取得。請把它們放在 config/services.php,並由環境變數提供,不要寫進程式碼。
請求內容的格式取決於時間。在客戶最後一則訊息後的 24 小時內,可以使用 text 物件,如下所示。超出這個時限後,Meta 只接受 template 物件,並須指明已核准的範本及其語言,自由格式訊息會以錯誤 131047 失敗。回應是查看問題的地方:失敗的呼叫會回傳 error 物件,其中的 code 說明出了什麼問題,所以請記錄下來,不要丟棄。
請求中沒有任何內容是 EasyCoexistence 專屬的。這就是 Meta 官方文件中的呼叫,所以任何 PHP HTTP 用戶端,無論是 Guzzle 還是原生 cURL,運作方式都一樣。
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.whatsapp.token'))
->post('https://graph.facebook.com/v25.0/'
. config('services.whatsapp.phone_number_id') . '/messages', [
'messaging_product' => 'whatsapp',
'to' => '14155550188', // digits only, with country code
'type' => 'text',
'text' => ['body' => 'Thanks, we are on it.'],
]);
if ($response->failed()) {
// error.code 131047: outside the window, send a template instead
logger()->warning('WhatsApp send failed', $response->json('error', []));
}正式環境的 Laravel 處理程式還需要什麼?
需要真正的隊列、去重,以及時限檢查。
隊列之所以重要,是因為 Meta 會在回應過慢時重試。只有當隊列連接是真正的連接,而且有 worker 在運行時,HandleWhatsAppEvent::dispatch() 才能迅速返回。使用 sync 驅動程式時,作業會在請求內執行,一次較慢的模型呼叫或資料庫寫入,就會變成重複傳送。
請以訊息 ID 去重,也就是每則訊息中的 wamid,因為重試是常態,而非例外。在該欄位建立唯一索引,或使用以它為鍵的快取鎖,就已足夠。
另外,請記錄每位聯絡人最後一次發訊息的時間。有了這個時間戳記,傳送流程就能自行選擇 text 或 template,而不必等客戶已經在等候時,才從錯誤 131047 得知。在手機的 WhatsApp Business 應用程式輸入的回覆,會以 echo 事件的形式送到同一條路由,所以處理程式也能知道何時有人接手了對話。
常見錯誤
- 以點號讀取 hub.challenge。PHP 已將它改名為 hub_challenge,處理程式因此回傳空白內容,Meta 永遠無法完成驗證。
- 把 Webhook 放在 routes/web.php。Meta 的 POST 沒有 CSRF 權杖,會以 419 被拒絕。
- 分派到 sync 隊列。作業在 200 回應之前執行,處理過慢會被重試,訊息因而被處理兩次。
- 以 response()->json() 回傳 challenge。值周圍的引號會令 Meta 的檢查失敗。
在 easycoexistence.com 連接號碼,把 Webhook 目的地設為您的 Laravel 路由(金鑰放在路徑中),然後從控制台讀取電話號碼 ID 和存取權杖。每個號碼每月 US$ 9 起,隨號碼增加降至 US$ 1,首 3 天免費。
常見問題
有 PHP SDK 嗎?
我們沒有提供,也不需要。這個 API 屬於 Meta,由 Meta 撰寫文件,Laravel 的 HTTP 用戶端或 Guzzle 可以直接呼叫。
不用 Laravel 也可以嗎?
可以。在純 PHP 中讀取 $_GET['hub_challenge'],以 text/plain 輸出,並用 cURL 傳送。底線規則適用於所有 PHP 框架。
為什麼 URL 的路徑中需要金鑰?
因為這是唯一行得通的驗證方式。Meta 的簽署使用 Tech Provider 的應用程式密鑰,而該密鑰從不分享,所以 URL 中的金鑰才能阻止陌生人傳送偽造事件。
團隊仍然可以使用手機嗎?
可以。Coexistence 讓 WhatsApp Business 應用程式繼續在該號碼上運作,而從手機傳送的訊息會以 echo 事件的形式送到您的路由。
繼續閱讀
準備開始嗎?
幾分鐘內設定 WhatsApp Coexistence,而非幾個月。應用程式在手機上仍可繼續運作。
開始免費試用驗證日期