ابدأ بسرعة: أول رسالة في 3 خطوات
اعمل مفتاح API، وابعت رسالة واتساب من رقمك، واستقبل أول ويب هوك موقّع — في حوالي عشر دقايق.
قبل ما تبدأ
محتاج حساب مرسلات باقته فيها API، ورقم مربوط في الأرقام (اعمل scan للـ QR من واتساب ← الأجهزة المرتبطة). كل الطلبات اللي تحت حقيقية: بتستخدم رقمك وباقتك.
1. اعمل مفتاح API
- افتح الإعدادات ← مفاتيح API في لوحة التحكم واضغط مفتاح جديد (لصاحب الحساب بس).
- سمّيه باسم النظام اللي هيستخدمه (مثلًا «سيرفر المتجر») واختار قالب إرسال الرسايل — بيدّي
messages:sendوmessages:readبس. - اختار الأرقام اللي يقدر يستخدمها (كلها أو بعضها)، ولو حابب عناوين IP السيرفرات المسموح لها.
- انسخ المفتاح (
wsl_live_…). بيظهر مرة واحدة؛ مرسلات بتحتفظ بـ hash بس ومش هتقدر تعرضه تاني أبدًا.
خلّي المفتاح في متغير بيئة على السيرفر بتاعك، عمره ما يكون في متصفح أو تطبيق موبايل. اتأكد إنه شغال — GET /v2/me مش محتاج صلاحية وبيقولك المفتاح يقدر يعمل إيه:
curl -X GET "https://mursilat.com/api/v2/me" \
-H "Authorization: Bearer $WASLA_API_KEY"const res = await fetch('https://mursilat.com/api/v2/me', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WASLA_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);import os
import requests
headers = {"Authorization": f"Bearer {os.environ['WASLA_API_KEY']}"}
res = requests.get(
"https://mursilat.com/api/v2/me",
headers=headers,
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data)<?php
$ch = curl_init('https://mursilat.com/api/v2/me');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WASLA_API_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) { throw new Exception($data['error']['code'] . ': ' . $data['error']['message']); }
print_r($data);2. ابعت أول رسالة
هات الـ id بتاع رقمك (المفتاح المقصور على أرقام معينة بيعرضها هي بس)، وبعدين ابعت. Idempotency-Key إجباري في الإرسال: لو الطلب عمل timeout، ابعته تاني بنفس المفتاح ومش هيتبعت مرتين أبدًا.
curl -X GET "https://mursilat.com/api/v2/numbers" \
-H "Authorization: Bearer $WASLA_API_KEY"const res = await fetch('https://mursilat.com/api/v2/numbers', {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.WASLA_API_KEY}`,
},
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);import os
import requests
headers = {"Authorization": f"Bearer {os.environ['WASLA_API_KEY']}"}
res = requests.get(
"https://mursilat.com/api/v2/numbers",
headers=headers,
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data)<?php
$ch = curl_init('https://mursilat.com/api/v2/numbers');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WASLA_API_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) { throw new Exception($data['error']['code'] . ': ' . $data['error']['message']); }
print_r($data);curl -X POST "https://mursilat.com/api/v2/messages" \
-H "Authorization: Bearer $WASLA_API_KEY" \
-H "Idempotency-Key: order-1042-confirm" \
-H "Content-Type: application/json" \
-d '{
"numberId": "cm1abcd2345efgh6789ijkl",
"to": "201001234567",
"type": "text",
"text": "طلبك اتشحن 🚚 *شكرًا*",
"reference": "order-1042"
}'const res = await fetch('https://mursilat.com/api/v2/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WASLA_API_KEY}`,
'Idempotency-Key': 'order-1042-confirm',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"numberId": "cm1abcd2345efgh6789ijkl",
"to": "201001234567",
"type": "text",
"text": "طلبك اتشحن 🚚 *شكرًا*",
"reference": "order-1042"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);import os
import requests
headers = {"Authorization": f"Bearer {os.environ['WASLA_API_KEY']}", "Idempotency-Key": "order-1042-confirm"}
res = requests.post(
"https://mursilat.com/api/v2/messages",
headers=headers,
json={
"numberId": "cm1abcd2345efgh6789ijkl",
"to": "201001234567",
"type": "text",
"text": "طلبك اتشحن 🚚 *شكرًا*",
"reference": "order-1042"
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data)<?php
$ch = curl_init('https://mursilat.com/api/v2/messages');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WASLA_API_KEY'),
'Idempotency-Key: order-1042-confirm',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numberId' => 'cm1abcd2345efgh6789ijkl',
'to' => '201001234567',
'type' => 'text',
'text' => 'طلبك اتشحن 🚚 *شكرًا*',
'reference' => 'order-1042'
], JSON_UNESCAPED_UNICODE),
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) { throw new Exception($data['error']['code'] . ': ' . $data['error']['message']); }
print_r($data);ده بيبعت رسالة واتساب حقيقية
ابعت لموبايلك أنت الأول. كل رسالة بتعدّي على حماية الرقم: وقت الضغط بيرجعلك status: "queued" ومعاه estimatedDelayMs، والرسايل للأرقام الجديدة بتتبعت بفواصل.
3. استقبل ويب هوك
إيصالات التسليم وردود العملاء وإجابات الأزرار بتوصل لعنوان HTTPS بتاعك، موقّعة. سجّل رابط واحد لكل أرقامك (محتاج مفتاح فيه webhooks:write — اعمل مفتاح تاني ليه، أو ضيف الصلاحية):
curl -X PUT "https://mursilat.com/api/v2/webhook" \
-H "Authorization: Bearer $WASLA_API_KEY" \
-H "Idempotency-Key: order-1042-confirm" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/mursilat",
"events": null
}'const res = await fetch('https://mursilat.com/api/v2/webhook', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.WASLA_API_KEY}`,
'Idempotency-Key': 'order-1042-confirm',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://example.com/mursilat",
"events": null
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);import os
import requests
headers = {"Authorization": f"Bearer {os.environ['WASLA_API_KEY']}", "Idempotency-Key": "order-1042-confirm"}
res = requests.put(
"https://mursilat.com/api/v2/webhook",
headers=headers,
json={
"url": "https://example.com/mursilat",
"events": None
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data)<?php
$ch = curl_init('https://mursilat.com/api/v2/webhook');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WASLA_API_KEY'),
'Idempotency-Key: order-1042-confirm',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/mursilat',
'events' => null
], JSON_UNESCAPED_UNICODE),
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) { throw new Exception($data['error']['code'] . ': ' . $data['error']['message']); }
print_r($data);الرد فيه secret التوقيع مرة واحدة. اتحقق بيه من كل طلب — احسب الـ HMAC على الـ body الخام، وقارن بوقت ثابت، وارفض الطوابع الزمنية القديمة:
// Node.js + Express. Use the RAW body: JSON.parse → JSON.stringify changes bytes and breaks the signature.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.WASLA_WEBHOOK_SECRET;
const seen = new Set(); // use Redis/DB in production
function verifyWaslaSignature(rawBody, header, secret, toleranceSec = 300) {
if (!header || !secret) return false;
const parts = {};
for (const pair of header.split(',')) {
const i = pair.indexOf('=');
if (i > 0) parts[pair.slice(0, i).trim()] = pair.slice(i + 1).trim();
}
const t = parts.t;
const v1 = parts.v1;
if (!/^\d+$/.test(t || '') || !/^[0-9a-f]{64}$/i.test(v1 || '')) return false;
// Reject old (replayed) or far-future timestamps.
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > toleranceSec) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(t + '.')
.update(rawBody) // the RAW bytes exactly as received, not re-serialized JSON
.digest();
const received = Buffer.from(v1, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
app.post('/webhooks/mursilat', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyWaslaSignature(req.body, req.get('X-Wasla-Signature'), SECRET)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200); // answer fast (within 10s); do the work after
if (seen.has(event.id)) return; // retries reuse the same event id
seen.add(event.id);
if (event.type === 'interactive.reply') {
console.log(event.data.reference, '→', event.data.reply.id);
}
});
app.listen(3000);<?php
// Use the RAW body (php://input), never a re-encoded json_decode() result.
function wasla_verify_signature(string $rawBody, string $header, string $secret, int $toleranceSec = 300): bool
{
$parts = [];
foreach (explode(',', $header) as $pair) {
$kv = explode('=', trim($pair), 2);
if (count($kv) === 2) {
$parts[$kv[0]] = $kv[1];
}
}
$t = $parts['t'] ?? '';
$v1 = $parts['v1'] ?? '';
if ($secret === '' || !ctype_digit($t) || !preg_match('/^[0-9a-f]{64}$/i', $v1)) {
return false;
}
// Reject old (replayed) or far-future timestamps.
if (abs(time() - (int) $t) > $toleranceSec) {
return false;
}
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, strtolower($v1)); // constant-time
}
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_WASLA_SIGNATURE'] ?? '';
if (!wasla_verify_signature($rawBody, $header, getenv('WASLA_WEBHOOK_SECRET') ?: '')) {
http_response_code(401);
exit('invalid signature');
}
$event = json_decode($rawBody, true);
http_response_code(200); // answer fast (within 10s)
// Retries reuse the same event id — store it and skip duplicates.
if ($event['type'] === 'interactive.reply') {
error_log($event['data']['reference'] . ' -> ' . $event['data']['reply']['id']);
}# Python 3 + Flask. Use the RAW body (request.get_data()), not request.json re-encoded.
import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["WASLA_WEBHOOK_SECRET"]
def verify_wasla_signature(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool:
parts = {}
for pair in (header or "").split(","):
key, sep, value = pair.strip().partition("=")
if sep:
parts[key] = value
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not secret or not t.isdigit() or len(v1) != 64:
return False
# Reject old (replayed) or far-future timestamps.
if abs(time.time() - int(t)) > tolerance_sec:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1.lower()) # constant-time
@app.post("/webhooks/mursilat")
def mursilat_webhook():
raw = request.get_data() # bytes exactly as received
if not verify_wasla_signature(raw, request.headers.get("X-Wasla-Signature", ""), SECRET):
abort(401)
event = json.loads(raw)
# Retries reuse the same event id — store it and skip duplicates.
if event["type"] == "interactive.reply":
print(event["data"]["reference"], "->", event["data"]["reply"]["id"])
return "", 200 # answer fast (within 10s)# Recompute a signature by hand (debugging). body.json = the raw request body, byte for byte.
# Header: X-Wasla-Signature: t=1760097603,v1=<hex>
T=1760097603
printf '%s.' "$T" | cat - body.json | openssl dgst -sha256 -hmac "$WASLA_WEBHOOK_SECRET" -hex
# → compare the hex digest with v1 (and check that T is within 5 minutes of now)بعدها ابعت حدث تجريبي وشوفه وهو بيوصل (وفي سجل التوصيل):
curl -X POST "https://mursilat.com/api/v2/webhook/test" \
-H "Authorization: Bearer $WASLA_API_KEY" \
-H "Idempotency-Key: order-1042-confirm" \
-H "Content-Type: application/json" \
-d '{
"numberId": "cm1abcd2345efgh6789ijkl"
}'const res = await fetch('https://mursilat.com/api/v2/webhook/test', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.WASLA_API_KEY}`,
'Idempotency-Key': 'order-1042-confirm',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"numberId": "cm1abcd2345efgh6789ijkl"
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
console.log(data);import os
import requests
headers = {"Authorization": f"Bearer {os.environ['WASLA_API_KEY']}", "Idempotency-Key": "order-1042-confirm"}
res = requests.post(
"https://mursilat.com/api/v2/webhook/test",
headers=headers,
json={
"numberId": "cm1abcd2345efgh6789ijkl"
},
timeout=30,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
print(data)<?php
$ch = curl_init('https://mursilat.com/api/v2/webhook/test');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('WASLA_API_KEY'),
'Idempotency-Key: order-1042-confirm',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'numberId' => 'cm1abcd2345efgh6789ijkl'
], JSON_UNESCAPED_UNICODE),
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) { throw new Exception($data['error']['code'] . ': ' . $data['error']['message']); }
print_r($data);تقدر تلصق اللي وصل لسيرفرك في فاحص الويب هوك وتتأكد من التوقيع جوه المتصفح.
الخطوة الجاية
- المصادقة — الصلاحيات وتقييد الأرقام وقائمة الـ IP وتغيير المفاتيح.
- الأخطاء — شكل الخطأ وكل كود ومعناه وتعمل إيه.
- أول حملة بأمان.
- الأزرار التفاعلية والبديل بالأرقام.