Tema
Eventi in uscita
LifeDome consegna eventi ai partner su due canali che condividono envelope e identificatori. Una sola implementazione di parsing + dedup copre entrambi.
| Canale | Trigger | Uso |
|---|---|---|
| Webhook push | LifeDome POST verso l'URL del partner a ogni evento | Real-time |
| Replay endpoint | Partner fa GET /api/v1/partner/outbound-events/ con paginazione cursor | Catch-up dopo downtime / riavvio |
L'envelope è lo stesso byte-per-byte. Il campo id (UUIDv7) è il valore che viaggia anche come header Idempotency-Key sul webhook: usalo come unica chiave di dedup, valida su entrambi i canali.
Configurazione
Tutto si configura sull'integrazione partner via portale (sezione "Accesso API" → card "Webhook in tempo reale") oppure su richiesta a LifeDome.
| Campo | Obbligatorio per abilitare | Note |
|---|---|---|
webhook_url | sì | HTTPS only. HTTP rifiutato. |
webhook_secret | sì | Segreto HMAC-SHA256 per la firma. Restituito una volta sola al momento della rotazione. |
webhook_event_types | no | Whitelist tipi. Vuoto ⇒ deriva dagli scope concessi all'integrazione. |
webhook_device_filter | no | Subset device. Vuoto ⇒ tutti i device condivisi. |
Disabilitare il webhook: svuotare webhook_url. Il replay endpoint resta disponibile.
Envelope
json
{
"id": "01HXYZ12345...",
"type": "alarm.sos",
"occurred_at": "2026-05-11T10:00:00Z",
"partner_integration_id": "01HX...",
"device": { "uid": "01HXDEV...", "imei": "353456789012345" },
"data": { "...": "shape per-tipo" }
}Tutti i timestamp ISO-8601 UTC con suffisso Z. Coordinate WGS84. Lo shape di data è specifico per type; sample canonici disponibili nello Schema OpenAPI.
Tipi di evento
| Tipo | Scope richiesto |
|---|---|
location.update | locations:read |
alarm.sos, alarm.fall, alarm.low_battery | events:read |
geofence.enter, geofence.exit | events:read + geofences:read |
health.snapshot | telemetry:read |
command.ack, command.failed | commands:read |
device.online, device.offline | devices:read |
message.received, message.sent, message.delivered, message.failed | messaging:read |
Un evento il cui scope non è nel token NON arriva al partner, anche se elencato in webhook_event_types. Il gate è applicato per riga sia in delivery che in replay.
Canale 1 — Webhook push
Request
LifeDome esegue un POST verso webhook_url con body JSON dell'envelope e questi header:
| Header | Valore |
|---|---|
Content-Type | application/json |
Idempotency-Key | uguale a envelope.id (UUIDv7) |
X-Signature | sha256=<hmac_hex> (vedi sotto) |
X-Event-Type | copia di envelope.type — utile per routing senza parsare il body |
X-Delivery-Attempt | numero del tentativo corrente, 1 … 5 |
Delivery semantics
- At-least-once: il partner DEVE dedup su
Idempotency-Key. - Retry: 5 tentativi, backoff esponenziale (1s, 5s, 30s, 2min, 10min).
- Timeout: 10s per tentativo.
- Successo: qualsiasi
2xxentro il timeout. - Errori retry-abili: 4xx (eccetto 410), 5xx, errori di rete / timeout.
410 Gonedal partner → la consegna passa immediatamente in DLQ, senza ulteriori retry. Usalo per segnalare il ritiro definitivo dell'URL; per pause temporanee preferire 503.- DLQ: dopo 5 fallimenti la riga viene parcheggiata; il recupero avviene via replay endpoint (sotto).
- Ordine: gli eventi POSSONO arrivare fuori ordine in retry. Ordinare per
occurred_at, dedup perid.
Verifica firma
python
import hmac, hashlib
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, request.headers["X-Signature"]):
raise Unauthorized()Il body usato per HMAC è il raw request body ricevuto, byte-per-byte. Non re-serializzare il JSON prima di calcolare la firma attesa: anche differenze di whitespace o ordinamento di chiavi farebbero fallire compare_digest.
Canale 2 — Replay endpoint
GET /api/v1/partner/outbound-events/
?since=<uuid7> # cursor esclusivo; ritorna righe con id > since
&types=<csv> # opzionale; CSV di tipi, intersecato con i sottoscritti
&limit=<n> # opzionale; default 100, cap 500
Authorization: Bearer <partner-token>Stesso token OAuth2 usato per le altre chiamate /api/v1/*. Nessuno scope nuovo. La chiamata rientra nel rate limit del partner come ogni altro endpoint REST. Il server applica webhook_event_types, webhook_device_filter e gli scope defensivamente anche in replay, così lo stream è equivalente al webhook.
Parametri:
since: UUIDv7 di una envelope ricevuta in precedenza. Omesso ⇒ parte dalla riga più vecchia ancora in finestra di retention.types: CSV di tipi da intersecare con i tipi sottoscritti dall'integrazione. Stringa vuota →400 invalid_filter(omettere il parametro per ricevere tutti i tipi). Tipi sconosciuti vengono silenziosamente ignorati.limit: intero in1..500. Fuori range o non intero →400 invalid_limit.
Response
json
{
"data": [ /* lista di envelope */ ],
"next_cursor": "<uuid7>|null",
"has_more": true
}next_cursor: id dell'ultima riga della pagina corrente. Passalo come?since=per la pagina successiva.nullquandohas_moreèfalse.has_more:truese ci sono altre righe disponibili oltre questa pagina.
Errori
| Status | code | Quando |
|---|---|---|
400 | invalid_cursor | since non è un UUIDv7 |
400 | invalid_limit | limit non intero o fuori range 1..500 |
400 | invalid_filter | types= presente ma vuoto |
410 | cursor_expired | since punta a una riga più vecchia della retention; response carry oldest_available_cursor |
Shape:
json
// 410 — cursor fuori finestra di retention (30 giorni)
{
"error": {
"code": "cursor_expired",
"message": "Cursor older than retention window.",
"oldest_available_cursor": "01HXOLDEST..."
}
}
// 400 — esempio cursor non UUID
{
"error": {
"code": "invalid_cursor",
"message": "since must be a UUIDv7 (the `id` from a previous response)."
}
}In caso di 410 ripartire dal valore oldest_available_cursor accettando il gap (eventi più vecchi della finestra di retention non sono più disponibili).
Recupero post-downtime
text
# pagina dall'ultimo id processato
GET /api/v1/partner/outbound-events/?since=01HXLASTACK&limit=200
→ { "data": [...200], "next_cursor": "01HXPAGE1END", "has_more": true }
# continua finché has_more=false
GET /api/v1/partner/outbound-events/?since=01HXPAGE1END&limit=200
→ { "data": [...87], "next_cursor": null, "has_more": false }Nessun header X-Signature sulle response di replay: il canale è autenticato via OAuth2 su TLS — identità dal token, integrità da TLS.
Idempotenza condivisa
L'id dell'envelope è UUIDv7 monotonicamente crescente. È al tempo stesso:
- la chiave primaria della riga lato LifeDome,
- l'
Idempotency-Keydel webhook, - il
cursordel replay.
Tienilo come unica chiave di dedup. Stesso id ricevuto via webhook e poi via replay (o viceversa, durante un catch-up) ⇒ scarta la copia duplicata.
Raw resource access (opzionale)
Per scenari di analytics o backfill su righe sorgente — non per il recupero di eventi — restano disponibili gli endpoint /api/v1/events/, /api/v1/devices/{uid}/locations/, /api/v1/devices/{uid}/telemetry/ e /api/v1/devices/{uid}/commands/ con paginazione cursor (?cursor=&order=asc). La finestra di look-back è quella della tabella sorgente, non quella del webhook (30 giorni).