Alertas e reencaminhamento
Um barco fora da rota, um estafeta em silêncio, uma carrinha parada tempo demais: avisado de imediato, onde a sua equipa já olha.
Nesta página
Os alertas vêm com o seguimento de frota. Configure-os na consola (Frota → Alertas) ou com a API de alertas.
O que o N-Ways vigia
| Alerta | Disparado quando | Evento |
|---|---|---|
| Sai da sua área | Um veículo em entrega sai da área da entrega (um círculo ou um polígono), ou das suas zonas permanentes. Também os barcos na lagoa. | asset.geofence_exit |
| Entra numa zona | Um veículo entra numa das suas zonas (um armazém, um mercado, uma zona interdita). | asset.geofence_enter |
| Deixa de dar sinal | Um veículo não enviou nenhuma posição durante o tempo da regra (30 minutos, por exemplo). | asset.silent |
| Para tempo demais | Um veículo ficou num raio pequeno durante o tempo da regra, fora dos seus armazéns. | asset.idle |
| De volta ao normal | Um alerta foi resolvido: por alguém na consola, pela API, ou sozinho quando o veículo voltou. | asset.alert_resolved |
Uma regra abrange todos os seus veículos, um grupo, alguns veículos ou um tipo (barcos, motas…). Avisa pelos canais que escolher (e-mail, notificação, webhook), fica em silêncio nas horas que definir e volta a lembrá-lo após o intervalo escolhido enquanto o alerta continuar aberto.
Reencaminhar os seus alertas
Comece pelas duas primeiras: não precisam de qualquer configuração.
1. E-mail e notificações
- E-mail: escolha na regra quem o recebe (todos os que têm uma função, pessoas escolhidas, ou qualquer endereço, como o do seu centro de operações).
- Notificações: na aplicação N-Ways Business, e em qualquer navegador onde alguém as tenha ativado na consola (Frota → Alertas → Avisar-me neste aparelho). No iPhone, é preciso primeiro adicionar a consola ao ecrã principal.
2. Excel: transferir todos os eventos
Todos os eventos, os mesmos que os webhooks recebem, numa folha de cálculo: o botão Transferir todos os eventos da consola (Frota → Alertas, ou Registos → Alertas), ou a API. O CSV abre no Excel e no Google Sheets com os nomes e o ₦ intactos; as horas são as 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: A_SUA_CHAVE_DE_SERVIDOR"Colunas: time_lagos, event, vehicle, vehicle_name, delivery, zone, alert, state, latitude, longitude, payload_json. Sem type, as entregas também são incluídas; format=json dá os eventos em bruto.
3. Uma folha do Google Sheets em tempo real
Uma folha que se preenche sozinha, linha a linha, à medida que os alertas acontecem. O Google Apps Script não lê os cabeçalhos dos pedidos, por isso não pode verificar a assinatura: proteja-a antes com um token aleatório longo no seu endereço.
- Crie uma folha do Google Sheets. Extensões → Apps Script e cole isto:
Code.gs
const TOKEN = 'cole-aqui-uma-cadeia-aleatoria-longa' 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') } - Implementar → Nova implementação → Aplicação Web; executar como Eu, acesso Qualquer pessoa. Copie o endereço da aplicação.
- Na consola, Definições → Webhooks → Adicionar endereço: o endereço seguido de
?token=e do seu token, eventosasset.*. Carregue em Enviar um teste: aparece uma linhaping.
4. Um grupo do Telegram
Os alertas como mensagens no seu grupo de despacho, através do mesmo Apps Script (ou de qualquer pequeno servidor).
- No Telegram, escreva a @BotFather:
/newbot, e guarde o token do bot. Adicione o bot ao seu grupo, envie lá uma mensagem e depois abrahttps://api.telegram.org/bot<token>/getUpdatespara ler ochat.iddo grupo (começa por-). - Acrescente isto ao script acima e chame
telegram(ev)emdoPostantes doreturn: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' ? `✅ Resolvido: ${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 }), }) } - Implemente uma nova versão (Implementar → Gerir implementações → Editar → Nova versão).
O WhatsApp só envia mensagens automáticas através da API WhatsApp Business, por isso precisa da sua própria conta num fornecedor (a Cloud API da Meta ou um fornecedor como a Twilio). Ligue-lhe um pequeno recetor como os de baixo.
5. O seu próprio servidor
Receba os eventos no seu servidor e faça com eles o que quiser. Verifique primeiro o cabeçalho X-NWays-Signature (t=<segundos unix>,v1=<hex>, HMAC-SHA256 de "<t>.<corpo em bruto>" com o segredo do endereço; detalhes), responda com 2xx em 10 segundos e faça o trabalho demorado depois.
// 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)
}Qualquer ferramenta que aceite um webhook (Zapier, Make, n8n…) também funciona.
Como é cada evento
Cada evento tem id, type, version (2026-10-16), createdAt e data. O data.alert de um alerta é o mesmo objeto que a consola e GET /alert-events mostram.
{
"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
}
}Quando alguém o resolve na consola ou pela API, by (quem) substitui auto.