Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/api/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ This log records changes to the TrakRF public API under `/api/v1/` that affect i

Changes to the TrakRF app itself are logged separately in the [release notes](../release-notes). The two are deliberately decoupled: the API version tracks the contract documented here, while the app version moves on its own release cadence, so an app release that changes nothing for integrators produces no entry on this page.

## Location history and movement events gain one-minute resolution {#minute-resolution-history}

Ships with app **v1.5.0**. No path, field name, response shape, or error envelope changes — this entry records a behavioral semantics change to data your code already consumes, the same reason webhook delivery semantics are logged here.

- **`GET /api/v1/assets/{asset_id}/history` returns at most one row per asset per minute.** The first location observed in a minute is the minute's recorded location; subsequent reads of the same asset in that minute do not add rows or change it. An operator save from the app is the exception — it overrides the current minute's recorded location. History was previously per-read; if your integration counted history rows as a proxy for read activity, that proxy is gone (it was never contractual). Row shape is unchanged.
- **`asset.moved` webhooks inherit the same resolution.** Passive readers emit at most one location change per asset per minute, and a mid-minute move delivers with the next minute's observation — up to 60 seconds after the physical move. An asset seen by two overlapping read zones can no longer produce an event storm: the minute is a built-in debounce window. Operator saves emit immediately, carrying the overridden location as `from_location`. See [Webhooks → Movement detection has one-minute resolution](./webhooks#movement-detection-has-one-minute-resolution).
- **`occurred_at` on save-driven events may carry the save's minute rather than the exact second.** Reader-driven events keep the scan instant. The recommended five-minute signature-timestamp tolerance absorbs this; a materially tighter window may not.

## v1.0 — Launch (2026-05-30)

Initial public API release. Stable contract for paths, field names, response shapes, and error envelopes per the [v1 stability commitment](./versioning).
Expand Down
2 changes: 2 additions & 0 deletions docs/api/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ If you only have surrogate ids in hand (you cached them from a previous create o

For per-asset history rather than current-state — "where has this asset been over the last seven days" — use `GET /api/v1/assets/{asset_id}/history?from=...&to=...`. The endpoint is the projection of the same scan-event stream with a different shape (time-series rows rather than current-state pairs). See [Pagination, filtering, sorting → Time range (history)](./pagination-filtering-sorting#time-range-history) for the `from`/`to` semantics.

History has **one-minute resolution**: at most one row per asset per minute, recording the first location observed in that minute (an operator save from the app overrides it). It is location history, not a per-read scan log — a fixed reader seeing a stationary tag hundreds of times an hour produces one row per minute, and the raw read stream is not exposed. The same resolution governs when [`asset.moved` webhooks](./webhooks#movement-detection-has-one-minute-resolution) fire.

## See also

- [Resource identifiers](./resource-identifiers) — how to address master-data rows by `id` or `external_key`.
Expand Down
25 changes: 18 additions & 7 deletions docs/api/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ The volume difference is substantial. In a representative fixed-reader deploymen

A scan that carries no location produces no event.

If you need the full scan stream rather than movements, poll [`GET /api/v1/assets/{asset_id}/history`](/api) instead.
If you need location history rather than movements, poll [`GET /api/v1/assets/{asset_id}/history`](/api) instead — it is recorded at the same one-minute resolution described below, not a per-read scan stream.

### Movement detection has one-minute resolution

Location history is recorded at one-minute granularity: an asset's first observation in a given minute sets that minute's recorded location, and later reads of the same asset in the same minute leave it unchanged. Movement events inherit that resolution, with three consequences for a receiver:

- **Passive readers produce at most one location change per asset per minute.** An asset that moves mid-minute emits its event on the next minute's observation — up to 60 seconds after the physical move, not instantly. Design for that latency; it is normal, not a delivery delay.
- **Overlapping read zones cannot flap.** An asset that two readers both see emits at most one movement event per minute, never an event per read — the minute acts as a built-in debounce window.
- **An operator save is the exception: it wins the minute and emits immediately.** A save from the app's Scan tab overrides the current minute's recorded location on the spot and emits its movement event right away, with the location it overrode as `from_location`. If a fixed reader still sees the asset afterward, the next minute's read re-asserts the reader's location — expect that return move about a minute later.

A same-minute re-save at an unchanged location emits nothing, consistent with the delta rule above.

## Registering a webhook

Expand Down Expand Up @@ -75,7 +85,7 @@ Delivered as `POST` with `Content-Type: application/json`.
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `event` | Always `asset.moved` in v1. Match on it anyway — more types will be added, and an unknown value should be ignored rather than treated as an error. |
| `delivery_id` | UUID identifying one detected move. Stable across the retries of a single delivery — use it as your idempotency key. |
| `occurred_at` | The scan instant that produced the move, in server time (RFC 3339, UTC). **Order by this**, not by arrival. |
| `occurred_at` | The observation that produced the move, in server time (RFC 3339, UTC). Reader-driven events carry the scan instant; events from an app save may carry the save's minute rather than the exact second ([minute resolution](#movement-detection-has-one-minute-resolution)). **Order by this**, not by arrival. |
| `data.asset.id` | The surrogate `id`, identical to the one the REST API returns for that asset. |
| `data.asset.external_key` | Your natural key for the asset. |
| `data.asset.name` | The asset's display name at the time of the move. |
Expand Down Expand Up @@ -155,7 +165,7 @@ Both compare in constant time — `crypto.timingSafeEqual` and `hmac.compare_dig

The timestamp is inside the signed material so that a captured delivery cannot be replayed indefinitely. Reject a delivery whose `X-TrakRF-Timestamp` falls outside a tolerance window — five minutes is a reasonable default — in addition to checking the signature.

**The signed timestamp is `occurred_at`, not the moment of transmission.** It is the scan instant, and it does not change when a delivery is retried. Your tolerance window is therefore measured from the physical scan, and it has to accommodate the pipeline latency between the scan and the delivery attempt, not just network time. Don't set it so tight that a normally-delayed event fails.
**The signed timestamp is `occurred_at`, not the moment of transmission.** It is the observation that produced the move — at minute resolution for save-driven events — and it does not change when a delivery is retried. Your tolerance window is therefore measured from the physical observation, and it has to accommodate both the pipeline latency between observation and delivery attempt and the up-to-a-minute timestamp rounding, not just network time. Don't set it so tight that a normally-delayed event fails; the five-minute default absorbs all of this comfortably.

## Delivery semantics

Expand Down Expand Up @@ -194,10 +204,11 @@ See [Authentication](./authentication) for how to authenticate that call.
In rough order of likelihood:

1. **Nothing moved.** Delivery is delta-only. Confirm against [`GET /api/v1/reports/asset-locations`](/api) that assets have actually changed location.
2. **The webhook is disabled.** Check the **Deliver events** checkbox on the Webhooks screen.
3. **The subscription lapsed.** Delivery stops for an organization that is no longer entitled, and nothing in your integration surfaces it. Check billing.
4. **Your endpoint started failing.** Use **Send test event** — it reports the status code your endpoint returned. Anything but a 2xx, including a redirect, is a failed delivery.
5. **The scans carry no location.** Reads that resolve to no location produce no event.
2. **The move just happened.** A move in the same minute as the asset's previous observation emits with the next minute's read — quiet for a minute is normal, not a failure. See [minute resolution](#movement-detection-has-one-minute-resolution).
3. **The webhook is disabled.** Check the **Deliver events** checkbox on the Webhooks screen.
4. **The subscription lapsed.** Delivery stops for an organization that is no longer entitled, and nothing in your integration surfaces it. Check billing.
5. **Your endpoint started failing.** Use **Send test event** — it reports the status code your endpoint returned. Anything but a 2xx, including a redirect, is a failed delivery.
6. **The scans carry no location.** Reads that resolve to no location produce no event.

## Not in v1

Expand Down
Loading