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
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
| Alerte | Levée quand | Événement |
|---|---|---|
| Sort de sa zone | Un 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 zone | Un 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 nouvelles | Un 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 longtemps | Un 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 normale | Une 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 -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.
- 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') } - Déployer → Nouveau déploiement → Application Web ; exécuter en tant que Moi, accès Tout le monde. Copiez l’adresse de l’application.
- Dans la console, Paramètres → Webhooks → Ajouter une adresse : l’adresse suivie de
?token=et de votre jeton, événementsasset.*. Appuyez sur Envoyer un test : une lignepingapparaî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).
- Dans Telegram, écrivez à @BotFather :
/newbot, et gardez le jeton du bot. Ajoutez le bot à votre groupe, envoyez-y un message, puis ouvrezhttps://api.telegram.org/bot<jeton>/getUpdatespour lire lechat.iddu groupe (il commence par-). - Ajoutez ceci au script ci-dessus, et appelez
telegram(ev)dansdoPostavant lereturn:Code.gsconst 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 }), }) } - Déployez une nouvelle version (Déployer → Gérer les déploiements → Modifier → Nouvelle version).
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.
// 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(',', '&', $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();
});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)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 "", 204package 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<<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, &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.
{
"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
}
}
}{
"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
}
}
}{
"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
}
}
}{
"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
}
}
}{
"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.