المرجع، وبيئة اختبار تلتزم به.
كل قاعدة في هذه الصفحة تعمل أيضا في المستكشف أدناه: حجم الصفحة، والحد الأقصى للطلبات، وما يحق للباقة كتابته، وكيف نميز المهمة المعادة. اكسرها هنا قبل أن تكسرها في بيئة الإنتاج.
- api.nml.sa/fleet/v1
- Base URL، وJSON عبر HTTPS
- 600/دقيقة
- لكل مفتاح، بما فيها الطلبات الفاشلة
- 1,000
- صفا في الصفحة، بحد أقصى
- 72 h
- نافذة إعادة إرسال webhook
جربه
بيئة تجريبية فيها 12 مركبة وساعة من المواقع: 2,520 صفا، أي 3 صفحة عند الحد الأقصى. تنقل بين الصفحات بالمؤشر، وأرسل المهمة نفسها مرتين، وبدل الباقة إلى Professional وحاول الكتابة، أو أرسل 650 طلبا دفعة واحدة وانظر أين يبدأ 429.
GET https://api.nml.sa/fleet/v1/positions?limit=1000
لم يرسل شيء بعد. ينفذ هذا المستكشف عقد API داخل متصفحك على أسطول تجريبي: لا مفتاح، ولا حساب، ولا شبكة.
المصادقة
مفتاح واحد لكل تكامل، ينشئه مسؤول، ولا يحمل إلا الصلاحيات التي يحتاجها. توضع المفاتيح في الترويسة، وأي مفتاح يظهر في URL يعد مسربا ويلغى.
curl https://api.nml.sa/fleet/v1/vehicles \
-H "Authorization: Bearer $SAQR_KEY" الصلاحيات
- read:fleet
- write:drivers
- write:jobs
- write:geofences
- write:webhooks
- read:exports
يمكن منح صلاحيات الكتابة في أي باقة، لكنها لا تعمل إلا في Command؛ وفي غيرها تعيد plan_required.
ترقيم الصفحات
مؤشرات cursor، لا أرقام صفحات. المؤشر يحدد موضعا في التدفق، فالصفوف التي تصل أثناء تنقلك بين الصفحات، كتعبئة لاحقة بعد منطقة بلا تغطية، تأتي بعد المؤشر بدلا من أن تزيح كل صفحة صفا واحدا.
let since = localStorage.cursor; // or wherever you keep it
let page;
do {
const r = await fetch(`https://api.nml.sa/fleet/v1/positions?limit=1000${since ? "&since=" + since : ""}`,
{ headers: { Authorization: `Bearer ${key}` } });
if (r.status === 429) { await sleep(Number(r.headers.get("Retry-After")) * 1000); continue; }
page = await r.json();
await save(page.data); // save first…
since = localStorage.cursor = page.next; // …then move the cursor
} while (page.has_more); احفظ الصفوف قبل أن تحفظ المؤشر. العكس يضيع صفحة كلما توقفت العملية بين الخطوتين.
endpoints
| endpoint | الصلاحية | Essential | Professional | Command |
|---|---|---|---|---|
| الأسطول | ||||
GET/vehicles المركبات، مع الرقم التسلسلي للوحدة والباقة وآخر موقع. | read:fleet | |||
GET/vehicles/{id} مركبة واحدة. | read:fleet | |||
GET/drivers السائقون وإسنادهم الحالي. | read:fleet | |||
POST/drivers أنشئ سائقا أو حدث بياناته من نظام الموارد البشرية لديك. | write:drivers | |||
| القياس عن بعد | ||||
GET/positions كل موقع بعد مؤشر محدد، في الأسطول كله. | read:fleet | |||
GET/trips الرحلات في نطاق زمني، مع المسافة والسائق. | read:fleet | |||
GET/events الأحداث نفسها التي ترسلها webhooks، للحاق بما فات. | read:fleet | |||
| العمليات | ||||
POST/jobs أرسل مهمة إلى مركبة. يتطلب Idempotency-Key. | write:jobs | |||
PATCH/jobs/{id} انقل نافذة زمنية، أو غير محطة، أو ألغ المهمة. | write:jobs | |||
GET/geofences السياجات الجغرافية وقواعد تنبيهاتها. | read:fleet | |||
POST/geofences أنشئ سياجا جغرافيا من مضلع. | write:geofences | |||
| التسليم | ||||
POST/webhooks/replay أعد إرسال الأحداث من نقطة خلال آخر 72 ساعة. | write:webhooks | |||
GET/exports ملفات التصدير المجدولة وحالتها. | read:exports | |||
الأخطاء
دائما { "error": { "code", "message" } }. Branch on the code; the message is for people and may change.
- 400
invalid_requestمعامل ناقص أو خارج النطاق، ومنه limit أكبر من حجم الصفحة. - 401
unauthenticatedلا مفتاح، أو مفتاح ملغى، أو مفتاح أرسل في URL بدلا من header. - 403
insufficient_scopeالمفتاح موجود لكنه لم يمنح هذا النطاق. - 403
plan_requiredعملية كتابة في باقة API فيها للقراءة فقط. - 404
not_foundid غير موجود، أو يخص حسابا آخر. لا نقول أيهما. - 409
idempotency_conflictأرسل Idempotency-Key نفسه مع محتوى طلب مختلف. - 429
rate_limitedأكثر من 600 طلب في الدقيقة على مفتاح واحد. وترويسة Retry-After تحدد موعد المحاولة التالية. - 503
unavailableصيانة مخططة أو حادثة. صفحة الحالة تكون قد أعلنت ذلك.
webhooks
-
trip.startedمفتاح التشغيل يعمل والمركبة تتحرك -
trip.completedمفتاح التشغيل متوقف منذ دقيقتين -
geofence.entered / exitedعبور حد رسمته -
alert.raisedأي قاعدة تنبيه، مع درجة خطورتها -
unit.silentلا بيانات بعد حد التقادم -
job.statusسائق ينقل مهمة إلى مرحلتها التالية في التطبيق
التحقق من التوقيع — عرض
// Every webhook carries: Saqr-Signature: t=<unix seconds>,v1=<hex>
// v1 is HMAC-SHA256 of "<t>.<raw body>" with your endpoint secret.
async function verify(raw: string, header: string, secret: string) {
const { t, v1 } = Object.fromEntries(
header.split(",").map((p) => p.split("=") as [string, string])
);
// Reject anything older than five minutes: a captured request replayed later.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]
);
const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${t}.${raw}`));
const hex = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
// Compare every character, so the time taken says nothing about where they differ.
let diff = hex.length ^ v1.length;
for (let i = 0; i < hex.length; i++) diff |= hex.charCodeAt(i) ^ (v1.charCodeAt(i) || 0);
return diff === 0;
}
// Deliveries are at-least-once: store evt ids and ignore a second arrival. حقول الموقع
- position
- سرعة
- heading
- ignition
- odometer
- fuel_pct
- driver_id
- accel
- gsm
- recorded_at
- received_at
الإصدارات في أربع قواعد
الإضافة لا تكسر التوافق
تصل حقول جديدة وendpoints جديدة وأنواع أحداث جديدة دون إشعار. تجاهل الحقول التي لا تعرفها.
كسر التوافق يعني إصدارا جديدا
حذف أي شيء أو إعادة تسميته أو تغيير نوعه لا يحدث إلا في v2، ولا يحدث أبدا داخل v1.
12 شهرا، ثم يتوقف
عند إيقاف دعم إصدار يظل يعمل 12 شهرا، ويحمل كل رد header باسم Sunset فيه التاريخ.
نبلغ مالك المفتاح
ترسل إشعارات إيقاف الدعم إلى المسؤول الذي أنشأ كل مفتاح ما زال يستدعي الإصدار القديم، لا إلى قائمة بريدية.