صقر

المرجع، وبيئة اختبار تلتزم به.

كل قاعدة في هذه الصفحة تعمل أيضا في المستكشف أدناه: حجم الصفحة، والحد الأقصى للطلبات، وما يحق للباقة كتابته، وكيف نميز المهمة المعادة. اكسرها هنا قبل أن تكسرها في بيئة الإنتاج.

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 الصلاحية EssentialProfessionalCommand
    الأسطول
    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_found id غير موجود، أو يخص حسابا آخر. لا نقول أيهما.
    • 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 فيه التاريخ.

    نبلغ مالك المفتاح

    ترسل إشعارات إيقاف الدعم إلى المسؤول الذي أنشأ كل مفتاح ما زال يستدعي الإصدار القديم، لا إلى قائمة بريدية.