---
title: "Segment change feed (poll)"
method: POST
path: "/api/v1/ackdb/observe/segments/{id}/changes"
tags: ["ackDB"]
---

# Segment change feed (poll)

`POST /api/v1/ackdb/observe/segments/{id}/changes`

## Request

- Base URL: `http://localhost:3009`
- URL: `http://localhost:3009/api/v1/ackdb/observe/segments/{id}/changes`
- Auth: HTTP bearer

Segment Sync S1: the change-log rows for a segment since cursor `N` - who entered / exited / re-entered. The response is ALWAYS a read of the committed append-only log (never an in-memory diff), so a poller that crashes before saving its cursor re-polls the same `since` and re-reads identical rows. The FIRST call bootstraps the snapshot and returns no rows (seed the initial members via /member-ids, so you never double-count). Runs under a non-blocking advisory lock (a concurrent call → 409). `since > maxSeq` → a 200 with `reconcile:true, reason:cursor_ahead`; `since` behind the prune watermark → 410 CURSOR_EXPIRED - both mean reconcile via /member-ids, never a silent empty-success. Derived segments are supported (their `filterHash` is the composite `own+base` - a base-filter edit flips it too); a cross-grain `?grain=` request → 400 (save the pivot as a derived segment to sync it); the sync tables not yet provisioned → 503. Poll again with `since = nextSince`. **Delivery is at-least-once** (a crash-safe re-poll re-reads the same rows; a /member-ids-seeded member can also reappear as an early enter) - dedup per member id.

## Path parameters

- `id` string, required

## Query parameters

- `since` integer

## Response `200`

A page of change rows (empty after bootstrap, or with a reconcile signal).

- AckdbSegmentChangesResponse
  - `segmentId` string, required
  - `filterHash` string, nullable, required: The membership's version key - changes iff a defining filter changed (detect an edit). Base segment: md5 of its filter. Derived segment: the composite `own+base`, so a base-filter edit flips it too. Null when edit detection is degraded (pre-migration DB) - null-propagating for derived, never a sentinel that looks like a real hash.
  - `maxSeq` integer, required: The segment's current highest seq. You are caught up when your last row's seq === maxSeq.
  - `minRetainedSeq` integer, required: Earliest seq still retained (prunedThroughSeq + 1).
  - `changes` AckdbSegmentChange[], required
    - `seq` integer, required: Global-monotonic log id. Advance your cursor to the max seq you have consumed.
    - `entityId` string, required: Member id - company entity_id or contact_id.
    - `kind` 'enter' | 'exit' | 'reenter', required
    - `at` string, date-time, required: When the row was appended.
  - `hasMore` boolean, required: More rows beyond this page - re-poll with since = nextSince.
  - `nextSince` integer, required: The cursor to poll with next (the last returned seq, or `since` if empty).
  - `bootstrapped` boolean: Present + true on the first call (snapshot built, no rows returned; seed via /member-ids).
  - `reconcile` boolean: Present + true when the cursor is unusable - re-seed via /member-ids (see `reason`).
  - `reason` 'cursor_ahead' | 'cursor_expired'

## Other responses

- `400`: Cross-grain `?grain=` request, a stale filter, or a malformed `since`.
- `404`: No segment with that id.
- `409`: LOCK_BUSY - another changes call holds this segment's lock; retry shortly.
- `410`: CURSOR_EXPIRED - `since` is behind the prune watermark; reconcile via /member-ids.
- `503`: SYNC_TABLES_UNAVAILABLE - the segment-sync migration has not run on this tenant yet.

---

[API](https://skmtc.dev/floqer/apis/floqer-public-api.md) - [All operations](https://skmtc.dev/floqer/apis/floqer-public-api/llms.txt) - [OpenAPI document](https://skmtc.dev/floqer/apis/floqer-public-api/revisions/fce50dc07b20?raw)
