N-Ways
Documentação para programadoresAlertas e reencaminhamento

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
Nota:

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

AlertaDisparado quandoEvento
Sai da sua áreaUm 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 zonaUm veículo entra numa das suas zonas (um armazém, um mercado, uma zona interdita).asset.geofence_enter
Deixa de dar sinalUm veículo não enviou nenhuma posição durante o tempo da regra (30 minutos, por exemplo).asset.silent
Para tempo demaisUm veículo ficou num raio pequeno durante o tempo da regra, fora dos seus armazéns.asset.idle
De volta ao normalUm 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
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.

  1. 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')
    }
  2. Implementar → Nova implementação → Aplicação Web; executar como Eu, acesso Qualquer pessoa. Copie o endereço da aplicação.
  3. Na consola, Definições → Webhooks → Adicionar endereço: o endereço seguido de ?token= e do seu token, eventos asset.*. Carregue em Enviar um teste: aparece uma linha ping.

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).

  1. No Telegram, escreva a @BotFather: /newbot, e guarde o token do bot. Adicione o bot ao seu grupo, envie lá uma mensagem e depois abra https://api.telegram.org/bot<token>/getUpdates para ler o chat.id do grupo (começa por -).
  2. Acrescente isto ao script acima e chame telegram(ev) em doPost antes do 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' ? `✅ 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 }),
      })
    }
  3. Implemente uma nova versão (Implementar → Gerir implementações → Editar → Nova versão).
Nota:

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.

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)
}

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.

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
  }
}

Quando alguém o resolve na consola ou pela API, by (quem) substitui auto.