N-Ways
Documentation développeursAlertes et transmission

Alertes et transmission

Un bateau hors de sa route, un livreur silencieux, une camionnette arrêtée trop longtemps : prévenu aussitôt, là où votre équipe regarde déjà.

Sur cette page
Remarque :

Les alertes sont fournies avec le suivi de flotte. Réglez-les dans la console (Flotte → Alertes) ou avec l’API des alertes.

Ce que N-Ways surveille

AlerteLevée quandÉvénement
Sort de sa zoneUn véhicule en livraison sort de la zone de la livraison (un cercle ou un polygone), ou de vos zones permanentes. Les bateaux sur la lagune aussi.asset.geofence_exit
Entre dans une zoneUn véhicule entre dans l’une de vos zones (un dépôt, un marché, une zone interdite).asset.geofence_enter
Ne donne plus de nouvellesUn véhicule n’a envoyé aucune position depuis la durée de la règle (30 minutes, par exemple).asset.silent
S’arrête trop longtempsUn véhicule est resté dans un petit rayon pendant la durée de la règle, hors de vos dépôts.asset.idle
Retour à la normaleUne alerte a été résolue : par quelqu’un dans la console, par l’API, ou d’elle-même quand le véhicule est revenu.asset.alert_resolved

Une règle couvre tous vos véhicules, un groupe, certains véhicules ou un type (bateaux, motos…). Elle prévient par les canaux choisis (e-mail, notification, webhook), se tait pendant les heures que vous fixez, et vous rappelle après l’intervalle choisi tant que l’alerte reste ouverte.

Transmettre vos alertes

Commencez par les deux premiers : ils ne demandent aucun réglage.

1. E-mail et notifications

  • E-mail : choisissez dans la règle qui le reçoit (tous ceux d’un rôle, des personnes choisies, ou n’importe quelle adresse, comme celle de votre centre d’opérations).
  • Notifications : dans l’application N-Ways Business, et dans tout navigateur où quelqu’un les a activées dans la console (Flotte → Alertes → Me prévenir sur cet appareil). Sur iPhone, il faut d’abord ajouter la console à l’écran d’accueil.

2. Excel : télécharger tous les événements

Chaque événement, les mêmes que ceux des webhooks, en tableur : le bouton Télécharger tous les événements de la console (Flotte → Alertes, ou Journaux → Alertes), ou l’API. Le CSV s’ouvre dans Excel et Google Sheets avec les noms et les ₦ intacts ; les heures sont celles de Lagos.

cURL
curl -o events.csv "https://nways.ng/maps/v1/fleet/events/export?format=csv&from=2026-10-01&type=asset." \
  -H "X-NWays-Api-Key: VOTRE_CLE_SERVEUR"

Colonnes : time_lagos, event, vehicle, vehicle_name, delivery, zone, alert, state, latitude, longitude, payload_json. Sans type, les livraisons sont incluses ; format=json donne les événements bruts.

3. Une feuille Google Sheets en direct

Une feuille qui se remplit toute seule, ligne par ligne, à chaque alerte. Google Apps Script ne lit pas les en-têtes des requêtes et ne peut donc pas vérifier la signature : protégez-la plutôt par un long jeton aléatoire dans son adresse.

  1. Créez une feuille Google Sheets. Extensions → Apps Script, puis collez ceci :
    Code.gs
    const TOKEN = 'collez-ici-une-longue-chaine-aleatoire'
    
    function doPost(e) {
      if (e.parameter.token !== TOKEN) return ContentService.createTextOutput('no')
      const ev = JSON.parse(e.postData.contents)
      const d = ev.data || {}
      const a = d.alert || {}
      const p = (a.detail && a.detail.position) || {}
      SpreadsheetApp.getActive().getSheets()[0].appendRow([
        new Date(ev.createdAt), ev.type, a.vehicleName || a.vehicleId || '', a.ruleName || '', a.state || '', p.lat || '', p.lng || '', ev.id,
      ])
      return ContentService.createTextOutput('ok')
    }
  2. Déployer → Nouveau déploiement → Application Web ; exécuter en tant que Moi, accès Tout le monde. Copiez l’adresse de l’application.
  3. Dans la console, Paramètres → Webhooks → Ajouter une adresse : l’adresse suivie de ?token= et de votre jeton, événements asset.*. Appuyez sur Envoyer un test : une ligne ping apparaît.

4. Un groupe Telegram

Les alertes en messages dans votre groupe de dispatch, par le même Apps Script (ou n’importe quel petit serveur).

  1. Dans Telegram, écrivez à @BotFather : /newbot, et gardez le jeton du bot. Ajoutez le bot à votre groupe, envoyez-y un message, puis ouvrez https://api.telegram.org/bot<jeton>/getUpdates pour lire le chat.id du groupe (il commence par -).
  2. Ajoutez ceci au script ci-dessus, et appelez telegram(ev) dans doPost avant le return :
    Code.gs
    const BOT = 'your-bot-token'
    const CHAT = '-1001234567890'
    
    function telegram(ev) {
      const a = (ev.data && ev.data.alert) || {}
      const text = ev.type === 'asset.alert_resolved' ? `✅ Résolue : ${a.kind}` : `⚠️ ${a.vehicleName || a.vehicleId}: ${a.ruleName || ev.type}`
      UrlFetchApp.fetch(`https://api.telegram.org/bot${BOT}/sendMessage`, {
        method: 'post',
        contentType: 'application/json',
        payload: JSON.stringify({ chat_id: CHAT, text }),
      })
    }
  3. Déployez une nouvelle version (Déployer → Gérer les déploiements → Modifier → Nouvelle version).
Remarque :

WhatsApp n’envoie de messages automatiques que par l’API WhatsApp Business : il vous faut votre propre compte chez un fournisseur (l’API Cloud de Meta ou un fournisseur comme Twilio). Branchez-y un petit récepteur comme ceux ci-dessous.

5. Votre propre serveur

Recevez les événements sur votre serveur et faites-en ce que vous voulez. Vérifiez d’abord l’en-tête X-NWays-Signature (t=<secondes unix>,v1=<hex>, HMAC-SHA256 de "<t>.<corps brut>" avec le secret de l’adresse ; détails), répondez par un 2xx en 10 secondes, et faites le travail long ensuite.

PHP (Laravel)
// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/nways/events', function (Request $request) {
    $raw = $request->getContent();
    parse_str(str_replace(',', '&amp;', $request->header('X-NWays-Signature', '')), $sig);
    if (abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
        abort(401);
    }
    $want = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $raw, config('services.nways.webhook_secret'));
    if (! hash_equals($want, $sig['v1'] ?? '')) {
        abort(401);
    }
    $event = json_decode($raw, true);
    if ($event['type'] === 'asset.geofence_exit') {
        \Log::warning('Left its area', $event['data']['alert']);
    }
    return response()->noContent();
});
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'
import { createServer } from 'node:http'

const SECRET = process.env.NWAYS_WEBHOOK_SECRET

function verified(raw, header) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
  const want = createHmac('sha256', SECRET).update(`${parts.t}.${raw}`).digest('hex')
  return want.length === parts.v1?.length && timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1))
}

createServer((req, res) => {
  let raw = ''
  req.on('data', (c) => (raw += c))
  req.on('end', () => {
    if (!verified(raw, req.headers['x-nways-signature'] ?? '')) return res.writeHead(401).end()
    const event = JSON.parse(raw)
    if (event.type.startsWith('asset.')) console.log(event.type, event.data.alert?.vehicleName)
    res.writeHead(204).end()
  })
}).listen(8080)
Python (Flask)
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["NWAYS_WEBHOOK_SECRET"].encode()


@app.post("/nways/events")
def events():
    raw = request.get_data()
    parts = dict(p.split("=", 1) for p in request.headers.get("X-NWays-Signature", "").split(",") if "=" in p)
    if abs(time.time() - int(parts.get("t", 0))) > 300:
        abort(401)
    want = hmac.new(SECRET, parts["t"].encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(want, parts.get("v1", "")):
        abort(401)
    event = request.get_json()
    if event["type"] == "asset.silent":
        print("Gone silent:", event["data"]["alert"]["vehicleName"])
    return "", 204
Go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"math"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

var secret = []byte(os.Getenv("NWAYS_WEBHOOK_SECRET"))

func verified(raw []byte, header string) bool {
	parts := map[string]string{}
	for _, p := range strings.Split(header, ",") {
		if k, v, ok := strings.Cut(p, "="); ok {
			parts[k] = v
		}
	}
	t, err := strconv.ParseInt(parts["t"], 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-t)) > 300 {
		return false
	}
	m := hmac.New(sha256.New, secret)
	m.Write([]byte(parts["t"] + "."))
	m.Write(raw)
	return hmac.Equal([]byte(hex.EncodeToString(m.Sum(nil))), []byte(parts["v1"]))
}

func main() {
	http.HandleFunc("POST /nways/events", func(w http.ResponseWriter, r *http.Request) {
		raw, _ := io.ReadAll(io.LimitReader(r.Body, 1&lt;&lt;20))
		if !verified(raw, r.Header.Get("X-NWays-Signature")) {
			http.Error(w, "bad signature", http.StatusUnauthorized)
			return
		}
		var ev struct {
			Type string          `json:"type"`
			Data json.RawMessage `json:"data"`
		}
		_ = json.Unmarshal(raw, &amp;ev)
		w.WriteHeader(http.StatusNoContent)
	})
	http.ListenAndServe(":8080", nil)
}

Tout outil qui accepte un webhook (Zapier, Make, n8n…) marche aussi.

À quoi ressemble chaque événement

Chaque événement a id, type, version (2026-10-16), createdAt et data. Le data.alert d’une alerte est le même objet que celui de la console et de GET /alert-events.

asset.geofence_exit
{
  "id": "evt_9mT2qfX4LwP0aZk1sYb7",
  "type": "asset.geofence_exit",
  "version": "2026-10-16",
  "createdAt": "2026-10-16T09:42:11Z",
  "data": {
    "alert": {
      "id": "alr_5c1e2b7a90d4",
      "ruleId": "rule_lagoon",
      "ruleName": "Boats stay on their route",
      "kind": "geofence_exit",
      "vehicleId": "boat-02",
      "vehicleName": "Boat 2",
      "state": "active",
      "detail": { "taskId": "FERRY-7", "area": "dispatch", "position": { "lat": 6.4489, "lng": 3.4542 }, "at": "2026-10-16T09:42:05Z" },
      "startedAt": "2026-10-16T09:42:05Z",
      "notifications": 1
    }
  }
}
asset.geofence_enter
{
  "id": "evt_Qe8sNw1rT6yU3iO0pLk2",
  "type": "asset.geofence_enter",
  "version": "2026-10-16",
  "createdAt": "2026-10-16T10:05:40Z",
  "data": {
    "alert": {
      "id": "alr_77b0f1c2e3a4",
      "ruleId": "rule_market",
      "ruleName": "Into Balogun market",
      "kind": "geofence_enter",
      "vehicleId": "bike-11",
      "vehicleName": "Bike 11",
      "subject": "zone:balogun",
      "state": "active",
      "detail": { "zoneId": "balogun", "zoneName": "Balogun market", "position": { "lat": 6.4592, "lng": 3.3898 }, "at": "2026-10-16T10:05:37Z" },
      "startedAt": "2026-10-16T10:05:37Z",
      "notifications": 1
    }
  }
}
asset.silent
{
  "id": "evt_Hb3vC7xZ0mQ9wE4rT1yU",
  "type": "asset.silent",
  "version": "2026-10-16",
  "createdAt": "2026-10-16T11:31:02Z",
  "data": {
    "alert": {
      "id": "alr_0a9b8c7d6e5f",
      "ruleId": "rule_silent",
      "ruleName": "Anyone silent for 30 min",
      "kind": "silent",
      "vehicleId": "bike-04",
      "vehicleName": "Bike 4",
      "state": "active",
      "detail": { "lastSeenAt": "2026-10-16T11:00:58Z", "silentS": 1800, "position": { "lat": 6.6018, "lng": 3.3515 } },
      "startedAt": "2026-10-16T11:31:00Z",
      "notifications": 1
    }
  }
}
asset.idle
{
  "id": "evt_Lp6kJ2hG8fD4sA0zX3cV",
  "type": "asset.idle",
  "version": "2026-10-16",
  "createdAt": "2026-10-16T13:20:15Z",
  "data": {
    "alert": {
      "id": "alr_1f2e3d4c5b6a",
      "ruleId": "rule_idle",
      "ruleName": "Stopped 20 min outside the depot",
      "kind": "idle",
      "vehicleId": "van-01",
      "vehicleName": "Van 1",
      "state": "active",
      "detail": { "since": "2026-10-16T13:00:09Z", "idleS": 1200, "radiusM": 50, "position": { "lat": 6.5244, "lng": 3.3792 } },
      "startedAt": "2026-10-16T13:20:10Z",
      "notifications": 1
    }
  }
}
asset.alert_resolved
{
  "id": "evt_Ys5tR8eW2qA6zX9cV1bN",
  "type": "asset.alert_resolved",
  "version": "2026-10-16",
  "createdAt": "2026-10-16T13:41:00Z",
  "data": {
    "alert": { "id": "alr_1f2e3d4c5b6a", "kind": "idle", "ruleId": "rule_idle" },
    "resolvedAt": "2026-10-16T13:41:00Z",
    "auto": true
  }
}

Quand quelqu’un la résout dans la console ou par l’API, by (qui) remplace auto.