EVENTS

Events & webhooks

loc_mesh.events.* PARTIALLY AVAILABLE

Mesh tells you what happened rather than making you ask. Every meaningful change — a stop completed, a package moved, a geofence crossed — is published as an event and delivered to your endpoint as a signed POST. This page covers the shape of what arrives and the guarantees around it.

What an event is

An event is a past fact. It records something that already happened and cannot be rejected, retried or argued with — stop.completed means the stop was completed, not that completing it has been requested.

That shape has two consequences worth knowing before you build against it:

  • Events fan out. Any number of subscribers can receive the same event, and Mesh does not care who is listening. Adding a subscriber changes nothing for the others.
  • Events are never replayed as instructions. If you need to make something happen, call the API. Events are the outbound half of the platform only.
There is a second, internal surface

The platform also runs a command surface (loc_mesh.commands.*) for imperative intents consumed by exactly one handler. It is internal plumbing and is not exposed to tenants — nothing on it reaches your webhook, and you cannot subscribe to it. Everything in these docs is the events surface.

The envelope

Every event, of every type, arrives wrapped in the same envelope. The fields below are the contract; the per-event payload sits inside payload and varies by event_type.

FieldTypeRequiredNotes
event_idstring uuid✓Unique per event. Consumers may dedup on this.
event_typestring✓Dotted, lowercase discriminator: stop.completed, run.started, package.moved, ... Drives which branch of the `payload` union is set. The canonical name consumers key on (the PascalCase Avro record name is the same event by form only — see the generated eventTypes map).
aggregate_typestring✓stop, run, order, package, user, note, ... Stable per producing context. (Driver is not its own aggregate — driver identity rides as an optional driver_id on the relevant payloads.)
aggregate_idstring✓Kafka partition key. Same field set by the Outbox SMT as the message key.
versionlong✓Monotonic per-aggregate sequence. Hot-cache CAS compares incoming > stored.
tenant_idstring✓Tenant scope for downstream routing and authorisation.
occurred_atlong timestamp-millis✓Wall-clock at which the writer committed. NOT the ordering key — use `version`.
correlation_idstring | null uuid—Trace ID propagated from the originating request.
client_correlation_idstring | null—Optional opaque client-supplied correlation id (X-Correlation-Id). Passthrough only — never used for auth/routing/dedup.
causation_idstring | null uuid—The event or command that caused this one, if any.
payloadunion—Per-event-type record. The union extends as new event types are added in schemas/<context>/*.avdl. Adding a branch is BACKWARD-compatible.
version is the ordering key, not occurred_at

occurred_at is wall-clock at the writer and is useful for display, not for sequencing. version is a monotonic per-aggregate counter — compare it, not timestamps, when you decide whether an event is newer than the state you hold.

How delivery works

Events are published to Kafka by the service that owns the change. From there:

  1. A processor matches the event against the active subscriptions for that tenant.
  2. A dispatcher signs the body with that subscription’s secret and POSTs it to your URL.
  3. Your endpoint answers. A 2xx ends the delivery; anything else decides whether it is retried.

Retries

Your responseWhat happens
2xxDelivered. Nothing further.
5xx, 429, or a network failureRetried with exponential backoff — 1s, then 3s, then 9s. After the last attempt the delivery goes to the dead-letter queue.
Any other 4xxDead-lettered immediately, with no retry. A 4xx says the request was wrong, and sending the identical request again cannot fix that.
Acknowledge fast, work afterwards

The dispatcher is waiting on your response. Return 200 as soon as you have the event safely written down, then do the real work out of band. An endpoint that processes synchronously turns a slow database into retries and, eventually, dead-lettered events.

Ordering and duplicates

Expect duplicates

Delivery is at-least-once. A retry that your endpoint actually received but failed to acknowledge in time arrives again, and the second copy is identical to the first. event_id is unique per event and stable across retries — key your idempotency on it.

Expect reordering across aggregates

Ordering holds per aggregate: two events for the same aggregate_id carry increasing version numbers. Across different aggregates there is no ordering guarantee at all, and retries can make that visible — a retried stop.arrived can land after a first-try stop.completed for a different stop.

The practical rule

Deduplicate on event_id. Order on aggregate_id + version. Treat an event whose version is not greater than what you already stored as something you have already handled.

What is live today

The mechanism and the catalogue are at different stages, and it is worth being precise about which is which.

  • The delivery pipeline is built. Subscriptions, HMAC signing, retry and dead-lettering all work as described above.
  • The catalogue is complete as a contract. All 33 event types across 7 topics have registered schemas, so the envelope and payload you will receive are settled.
  • Which events actually reach you depends on your modules. An event only flows if the module that raises it is live and enabled on your account. Several modules are still Planned — their event types are documented so you can build against them, but nothing emits them yet.

Each module page lists the events that module raises, and carries its own availability label. That is the place to check whether a given event type is something you can receive today.

GPS telemetry is a separate surface

Live driver positions do not arrive as domain events. Position fixes stream from the mobile app at a volume that would swamp a webhook endpoint, so they travel on their own telemetry topic and are read rather than pushed:

  • Current position — read the snapshot endpoint on the tracking API. Sub-millisecond, served from cache.
  • Where a driver has been — read the bounded history endpoint over a time window.

What does arrive as an event is the meaning derived from those positions — geofence.entered and geofence.exited are raised when a tracked subject crosses a boundary you defined. See Realtime GPS Tracking and Geofences.

If you want positions pushed rather than polled

That is the geofence pattern: define the boundaries that matter to you and subscribe to the crossings. Subscribing to raw position fixes is not offered, because at one fix per driver every few seconds it would be a denial-of-service against your own endpoint.

Where to go next