Complete integration examples
Copy complete backend examples for Node/Express, Next.js, Laravel, Python, and plain PHP.
Node and Express
Expose your own authenticated backend route. The browser calls your server; your server calls HookMessage with the API key.
import express from "express";
const app = express();
app.use(express.json());
app.post("/orders/:orderId/send-confirmation", async (req, res, next) => {
try {
const response = await fetch(`${process.env.HOOKMESSAGE_BASE_URL}/api/messages/send`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.HOOKMESSAGE_API_KEY!,
},
body: JSON.stringify({
sender_id: process.env.HOOKMESSAGE_SENDER_ID,
receiver_number: req.body.phone,
message: `Order ${req.params.orderId} is confirmed.`,
source_system: "express-api",
source_reference: req.params.orderId,
idempotency_key: `order:${req.params.orderId}:confirmed`,
}),
});
const data = await response.json();
if (!response.ok) return res.status(response.status).json(data);
return res.status(201).json({ messageId: data.message_id, status: data.status });
} catch (error) {
next(error);
}
});Next.js route handler
Keep the API key in server environment variables and return only your own safe response to the client.
export async function POST(request: Request) {
const body = await request.json();
const response = await fetch(`${process.env.HOOKMESSAGE_BASE_URL}/api/messages/send`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.HOOKMESSAGE_API_KEY!,
},
body: JSON.stringify({
sender_id: process.env.HOOKMESSAGE_SENDER_ID,
receiver_number: body.phone,
message: "Your request was received.",
source_system: "nextjs",
source_reference: body.requestId,
idempotency_key: `request:${body.requestId}:received`,
}),
});
const data = await response.json();
if (!response.ok) return Response.json(data, { status: response.status });
return Response.json({ messageId: data.message_id, status: data.status });
}Laravel service and controller
Wrap HookMessage behind a service so controllers do not duplicate headers, idempotency, or error handling.
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
final class HookMessageService
{
public function sendConfirmation(string $phone, string $orderId): array
{
$response = Http::baseUrl(config('services.hookmessage.url'))
->withHeaders(['x-api-key' => config('services.hookmessage.key')])
->post('/api/messages/send', [
'sender_id' => config('services.hookmessage.sender_id'),
'receiver_number' => $phone,
'message' => "Order {$orderId} is confirmed.",
'source_system' => 'laravel',
'source_reference' => $orderId,
'idempotency_key' => "order:{$orderId}:confirmed",
]);
$response->throw();
return $response->json();
}
}
Route::post('/orders/{order}/send-confirmation', function (Request $request, string $order, HookMessageService $hookMessage) {
return response()->json($hookMessage->sendConfirmation($request->string('phone'), $order));
});Python Flask and FastAPI
The same server-side pattern works in Flask or FastAPI. Keep the API key out of browser code.
import os
import requests
from flask import Flask, request, jsonify
from fastapi import FastAPI
flask_app = Flask(__name__)
fastapi_app = FastAPI()
def send_confirmation(phone: str, reference: str) -> dict:
response = requests.post(
f"{os.environ['HOOKMESSAGE_BASE_URL']}/api/messages/send",
headers={"x-api-key": os.environ["HOOKMESSAGE_API_KEY"]},
json={
"sender_id": os.environ["HOOKMESSAGE_SENDER_ID"],
"receiver_number": phone,
"message": "Your request was received.",
"source_system": "python",
"source_reference": reference,
"idempotency_key": f"request:{reference}:received",
},
timeout=20,
)
response.raise_for_status()
return response.json()
@flask_app.post("/send-confirmation")
def flask_send_confirmation():
payload = request.get_json()
return jsonify(send_confirmation(payload["phone"], payload["request_id"]))
@fastapi_app.post("/send-confirmation")
def fastapi_send_confirmation(payload: dict):
return send_confirmation(payload["phone"], payload["request_id"])Plain PHP endpoint
Plain PHP should still keep secrets server-side and return only safe response fields.
<?php
$input = json_decode(file_get_contents('php://input'), true);
$payload = json_encode([
'sender_id' => getenv('HOOKMESSAGE_SENDER_ID'),
'receiver_number' => $input['phone'],
'message' => 'Your request was received.',
'source_system' => 'plain-php',
'source_reference' => $input['request_id'],
'idempotency_key' => 'request:' . $input['request_id'] . ':received',
], JSON_THROW_ON_ERROR);
$ch = curl_init(rtrim(getenv('HOOKMESSAGE_BASE_URL'), '/') . '/api/messages/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'content-type: application/json',
'x-api-key: ' . getenv('HOOKMESSAGE_API_KEY'),
],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
http_response_code($status);
header('content-type: application/json');
echo $body;Need help?
Use Book Integration Help if you want setup help for OTP or automated customer messaging.
Next steps
Quickstart
A practical path from empty workspace to a working server-side integration.
Authentication and access
HookMessage separates customer API keys, dashboard sessions, and internal platform access.
Workspaces
A workspace is the tenant boundary for every customer resource.
Senders and sessions
A sender is the WhatsApp number used to deliver messages for a workspace.